From 6764d8074a3b0da48b9fcb963631043d6cd6de08 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Tue, 29 Apr 2025 11:02:52 +0200 Subject: [PATCH] Setting a theme style now automatically switches to that theme Signed-off-by: Hans Kokx --- .../services/reactive_theme/arcane_theme.dart | 66 ++----------------- .../reactive_theme_extensions.dart | 2 +- .../reactive_theme_service.dart | 49 ++++++++++---- .../reactive_theme_switcher.dart | 14 ++-- .../reactive_theme_service_test.dart | 14 ++-- 5 files changed, 61 insertions(+), 84 deletions(-) diff --git a/lib/src/services/reactive_theme/arcane_theme.dart b/lib/src/services/reactive_theme/arcane_theme.dart index 9f46352..d91ea3d 100644 --- a/lib/src/services/reactive_theme/arcane_theme.dart +++ b/lib/src/services/reactive_theme/arcane_theme.dart @@ -1,16 +1,15 @@ -import "dart:async"; - -import "package:arcane_framework/src/services/reactive_theme/reactive_theme_service.dart"; import "package:flutter/material.dart"; class ArcaneTheme extends InheritedWidget { final ThemeMode themeMode; final bool followSystem; + final ThemeData? theme; const ArcaneTheme({ - required this.themeMode, - required this.followSystem, required super.child, + this.themeMode = ThemeMode.light, + this.followSystem = false, + this.theme, super.key, }); @@ -21,60 +20,7 @@ class ArcaneTheme extends InheritedWidget { @override bool updateShouldNotify(ArcaneTheme oldWidget) { return themeMode != oldWidget.themeMode || - followSystem != oldWidget.followSystem; + followSystem != oldWidget.followSystem || + theme != oldWidget.theme; } - - /// Returns the singleton instance of the [ArcaneReactiveTheme] service. - ArcaneReactiveTheme get service => ArcaneReactiveTheme.I; - - /// Indicates whether the theme is currently set to follow the system theme. - bool get isFollowingSystemTheme => service.isFollowingSystemTheme; - - /// Provides a stream of [ThemeMode] changes that can be listened to for reactive updates. - Stream get themeChanges => service.themeChanges; - - /// Returns the currently active [ThemeMode]. - ThemeMode get currentTheme => service.currentTheme; - - /// Returns the [ThemeMode] currently set at the OS/system level. - ThemeMode get systemTheme => service.systemTheme; - - /// Returns the dark [ThemeData] configuration. - ThemeData get dark => service.dark; - - /// Returns a [ValueNotifier] containing the dark [ThemeData], allowing for reactive updates. - ValueNotifier get darkTheme => service.darkTheme; - - /// Returns the light [ThemeData] configuration. - ThemeData get light => service.light; - - /// Returns a [ValueNotifier] containing the light [ThemeData], allowing for reactive updates. - ValueNotifier get lightTheme => service.lightTheme; - - /// A shortcut to the [ArcaneReactiveTheme] function that switches the active theme mode. - /// - /// - [themeMode] (Optional): Specify which theme mode to switch to. - /// Otherwise, tries to determine whether to switch to light or dark mode, automatically. - ArcaneReactiveTheme Function({ThemeMode? themeMode}) get switchTheme => - service.switchTheme; - - /// A shortcut to the [ArcaneReactiveTheme] function that follows the system theme. - /// - /// - [context]: The [BuildContext] required to access system theme information. - void Function(BuildContext context) followSystemTheme( - BuildContext context, - ) => - service.followSystemTheme; - - /// A shortcut to the [ArcaneReactiveTheme] function that updates the dark theme configuration. - /// - /// The function accepts a [ThemeData] parameter to set as the new dark theme. - ArcaneReactiveTheme Function(ThemeData theme) get setDarkTheme => - service.setDarkTheme; - - /// A shortcut to the [ArcaneReactiveTheme] function that updates the light theme configuration. - /// - /// The function accepts a [ThemeData] parameter to set as the new light theme. - ArcaneReactiveTheme Function(ThemeData theme) get setLightTheme => - service.setLightTheme; } diff --git a/lib/src/services/reactive_theme/reactive_theme_extensions.dart b/lib/src/services/reactive_theme/reactive_theme_extensions.dart index aebc9e4..9ad45a2 100644 --- a/lib/src/services/reactive_theme/reactive_theme_extensions.dart +++ b/lib/src/services/reactive_theme/reactive_theme_extensions.dart @@ -24,6 +24,6 @@ extension ArcaneThemeContext on BuildContext { /// Get the current theme mode from the nearest ArcaneThemeInherited widget ThemeMode get themeMode { return ArcaneTheme.of(this)?.themeMode ?? - ArcaneReactiveTheme.I.currentTheme; + ArcaneReactiveTheme.I.currentThemeMode; } } diff --git a/lib/src/services/reactive_theme/reactive_theme_service.dart b/lib/src/services/reactive_theme/reactive_theme_service.dart index 2aa5760..316aeb2 100644 --- a/lib/src/services/reactive_theme/reactive_theme_service.dart +++ b/lib/src/services/reactive_theme/reactive_theme_service.dart @@ -38,26 +38,41 @@ class ArcaneReactiveTheme extends ArcaneService { }, ); - final StreamController _themeStreamController = + final StreamController _themeModeStreamController = StreamController.broadcast( + onCancel: () { + I._themeModeStreamController.close(); + }, + ); + + final StreamController _themeStreamController = + StreamController.broadcast( onCancel: () { I._themeStreamController.close(); }, ); /// Stream of `ThemeMode` changes that can be listened to for reactive UI updates. - Stream get themeChanges => I._themeStreamController.stream; + Stream get themeModeChanges => I._themeModeStreamController.stream; + + /// Stream of `ThemeData` changes that can be listened to for reactive UI updates. + Stream get themeDataChanges => I._themeStreamController.stream; /// Returns the `ThemeData` corresponding to the current system theme - ThemeMode get systemTheme => _currentSystemTheme; + ThemeMode get systemThemeMode => _currentSystemThemeMode; /// Tracks the current system theme mode - ThemeMode _currentSystemTheme = ThemeMode.system; + ThemeMode _currentSystemThemeMode = ThemeMode.system; - ThemeMode _currentTheme = ThemeMode.light; + ThemeMode _currentThemeMode = ThemeMode.light; /// The currently active theme mode (light or dark). - ThemeMode get currentTheme => _currentTheme; + ThemeMode get currentThemeMode => _currentThemeMode; + + ThemeData _currentTheme = ThemeData(); + + /// The currently active theme style. + ThemeData get currentTheme => _currentTheme; /// The `ThemeData` for the dark theme. final ValueNotifier _darkTheme = ValueNotifier(ThemeData.dark()); @@ -101,7 +116,7 @@ class ArcaneReactiveTheme extends ArcaneService { _updateTheme(themeMode); } else { _updateTheme( - currentTheme == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark, + currentThemeMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark, ); } @@ -124,10 +139,14 @@ class ArcaneReactiveTheme extends ArcaneService { ArcaneReactiveTheme followSystemTheme(BuildContext context) { _followingSystemTheme = true; - _currentSystemTheme = context.isDarkMode ? ThemeMode.dark : ThemeMode.light; - _systemStreamController.add(_currentSystemTheme); - _updateTheme(_currentSystemTheme); + _currentSystemThemeMode = + context.isDarkMode ? ThemeMode.dark : ThemeMode.light; + _systemStreamController.add(_currentSystemThemeMode); + _updateTheme(_currentSystemThemeMode); + final ThemeData theme = systemThemeMode == ThemeMode.dark ? dark : light; + _themeStreamController.add(theme); + _currentTheme = theme; notifyListeners(); return I; @@ -144,6 +163,8 @@ class ArcaneReactiveTheme extends ArcaneService { /// ``` ArcaneReactiveTheme setDarkTheme(ThemeData theme) { _darkTheme.value = theme; + _themeStreamController.add(theme); + _currentTheme = theme; notifyListeners(); return I; } @@ -159,6 +180,8 @@ class ArcaneReactiveTheme extends ArcaneService { /// ``` ArcaneReactiveTheme setLightTheme(ThemeData theme) { _lightTheme.value = theme; + _themeStreamController.add(theme); + _currentTheme = theme; notifyListeners(); return I; } @@ -173,12 +196,14 @@ class ArcaneReactiveTheme extends ArcaneService { _lightTheme.value = ThemeData.light(); _followingSystemTheme = false; _updateTheme(ThemeMode.light); + _themeStreamController.add(_lightTheme.value); + _currentTheme = _lightTheme.value; notifyListeners(); } /// Updates the current theme mode and broadcasts the change. void _updateTheme(ThemeMode themeMode) { - _currentTheme = themeMode; - _themeStreamController.add(themeMode); + _currentThemeMode = themeMode; + _themeModeStreamController.add(themeMode); } } diff --git a/lib/src/services/reactive_theme/reactive_theme_switcher.dart b/lib/src/services/reactive_theme/reactive_theme_switcher.dart index 94d053a..4913e26 100644 --- a/lib/src/services/reactive_theme/reactive_theme_switcher.dart +++ b/lib/src/services/reactive_theme/reactive_theme_switcher.dart @@ -16,27 +16,33 @@ class ArcaneThemeSwitcher extends StatefulWidget { } class _ArcaneThemeSwitcherState extends State { - late final StreamSubscription _subscription; + late final StreamSubscription _themeModeSubscription; + late final StreamSubscription _themeSubscription; @override void initState() { super.initState(); - _subscription = ArcaneReactiveTheme.I.themeChanges.listen((_) { + _themeModeSubscription = ArcaneReactiveTheme.I.themeModeChanges.listen((_) { + setState(() {}); + }); + _themeSubscription = ArcaneReactiveTheme.I.themeDataChanges.listen((_) { setState(() {}); }); } @override void dispose() { - _subscription.cancel(); + _themeModeSubscription.cancel(); + _themeSubscription.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return ArcaneTheme( - themeMode: ArcaneReactiveTheme.I.currentTheme, + themeMode: ArcaneReactiveTheme.I.currentThemeMode, followSystem: ArcaneReactiveTheme.I.isFollowingSystemTheme, + theme: ArcaneReactiveTheme.I.currentTheme, child: widget.child, ); } diff --git a/test/services/reactive_theme/reactive_theme_service_test.dart b/test/services/reactive_theme/reactive_theme_service_test.dart index 92e179f..68c6630 100644 --- a/test/services/reactive_theme/reactive_theme_service_test.dart +++ b/test/services/reactive_theme/reactive_theme_service_test.dart @@ -16,15 +16,15 @@ void main() { group("theme mode", () { test("initial mode is light", () { - expect(theme.currentTheme, equals(ThemeMode.light)); + expect(theme.currentThemeMode, equals(ThemeMode.light)); }); test("switchTheme toggles between light and dark", () { - expect(theme.currentTheme, equals(ThemeMode.light)); + expect(theme.currentThemeMode, equals(ThemeMode.light)); theme.switchTheme(); - expect(theme.currentTheme, equals(ThemeMode.dark)); + expect(theme.currentThemeMode, equals(ThemeMode.dark)); theme.switchTheme(); - expect(theme.currentTheme, equals(ThemeMode.light)); + expect(theme.currentThemeMode, equals(ThemeMode.light)); }); test("switching theme notifies listeners", () { @@ -66,7 +66,7 @@ void main() { }); theme.addListener(() { - currentTheme = theme.currentTheme; + currentTheme = theme.currentThemeMode; }); expect(currentTheme, ThemeMode.system); @@ -106,7 +106,7 @@ void main() { Arcane.theme.followSystemTheme(lightContext); await tester.pumpAndSettle(); - expect(theme.currentTheme, equals(ThemeMode.light)); + expect(theme.currentThemeMode, equals(ThemeMode.light)); await tester.pumpWidget( const MediaQuery( @@ -121,7 +121,7 @@ void main() { Arcane.theme.followSystemTheme(darkContext); await tester.pumpAndSettle(); - expect(theme.currentTheme, equals(ThemeMode.dark)); + expect(theme.currentThemeMode, equals(ThemeMode.dark)); }); }); });