Setting a theme style now automatically switches to that theme

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2025-04-29 11:02:52 +02:00
parent 3fd38d3b26
commit 6764d8074a
5 changed files with 61 additions and 84 deletions
@@ -1,16 +1,15 @@
import "dart:async";
import "package:arcane_framework/src/services/reactive_theme/reactive_theme_service.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
class ArcaneTheme extends InheritedWidget { class ArcaneTheme extends InheritedWidget {
final ThemeMode themeMode; final ThemeMode themeMode;
final bool followSystem; final bool followSystem;
final ThemeData? theme;
const ArcaneTheme({ const ArcaneTheme({
required this.themeMode,
required this.followSystem,
required super.child, required super.child,
this.themeMode = ThemeMode.light,
this.followSystem = false,
this.theme,
super.key, super.key,
}); });
@@ -21,60 +20,7 @@ class ArcaneTheme extends InheritedWidget {
@override @override
bool updateShouldNotify(ArcaneTheme oldWidget) { bool updateShouldNotify(ArcaneTheme oldWidget) {
return themeMode != oldWidget.themeMode || 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<ThemeMode> 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<ThemeData> 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<ThemeData> 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;
} }
@@ -24,6 +24,6 @@ extension ArcaneThemeContext on BuildContext {
/// Get the current theme mode from the nearest ArcaneThemeInherited widget /// Get the current theme mode from the nearest ArcaneThemeInherited widget
ThemeMode get themeMode { ThemeMode get themeMode {
return ArcaneTheme.of(this)?.themeMode ?? return ArcaneTheme.of(this)?.themeMode ??
ArcaneReactiveTheme.I.currentTheme; ArcaneReactiveTheme.I.currentThemeMode;
} }
} }
@@ -38,26 +38,41 @@ class ArcaneReactiveTheme extends ArcaneService {
}, },
); );
final StreamController<ThemeMode> _themeStreamController = final StreamController<ThemeMode> _themeModeStreamController =
StreamController<ThemeMode>.broadcast( StreamController<ThemeMode>.broadcast(
onCancel: () {
I._themeModeStreamController.close();
},
);
final StreamController<ThemeData> _themeStreamController =
StreamController<ThemeData>.broadcast(
onCancel: () { onCancel: () {
I._themeStreamController.close(); I._themeStreamController.close();
}, },
); );
/// Stream of `ThemeMode` changes that can be listened to for reactive UI updates. /// Stream of `ThemeMode` changes that can be listened to for reactive UI updates.
Stream<ThemeMode> get themeChanges => I._themeStreamController.stream; Stream<ThemeMode> get themeModeChanges => I._themeModeStreamController.stream;
/// Stream of `ThemeData` changes that can be listened to for reactive UI updates.
Stream<ThemeData> get themeDataChanges => I._themeStreamController.stream;
/// Returns the `ThemeData` corresponding to the current system theme /// Returns the `ThemeData` corresponding to the current system theme
ThemeMode get systemTheme => _currentSystemTheme; ThemeMode get systemThemeMode => _currentSystemThemeMode;
/// Tracks the current system theme mode /// 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). /// 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. /// The `ThemeData` for the dark theme.
final ValueNotifier<ThemeData> _darkTheme = ValueNotifier(ThemeData.dark()); final ValueNotifier<ThemeData> _darkTheme = ValueNotifier(ThemeData.dark());
@@ -101,7 +116,7 @@ class ArcaneReactiveTheme extends ArcaneService {
_updateTheme(themeMode); _updateTheme(themeMode);
} else { } else {
_updateTheme( _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) { ArcaneReactiveTheme followSystemTheme(BuildContext context) {
_followingSystemTheme = true; _followingSystemTheme = true;
_currentSystemTheme = context.isDarkMode ? ThemeMode.dark : ThemeMode.light; _currentSystemThemeMode =
_systemStreamController.add(_currentSystemTheme); context.isDarkMode ? ThemeMode.dark : ThemeMode.light;
_updateTheme(_currentSystemTheme); _systemStreamController.add(_currentSystemThemeMode);
_updateTheme(_currentSystemThemeMode);
final ThemeData theme = systemThemeMode == ThemeMode.dark ? dark : light;
_themeStreamController.add(theme);
_currentTheme = theme;
notifyListeners(); notifyListeners();
return I; return I;
@@ -144,6 +163,8 @@ class ArcaneReactiveTheme extends ArcaneService {
/// ``` /// ```
ArcaneReactiveTheme setDarkTheme(ThemeData theme) { ArcaneReactiveTheme setDarkTheme(ThemeData theme) {
_darkTheme.value = theme; _darkTheme.value = theme;
_themeStreamController.add(theme);
_currentTheme = theme;
notifyListeners(); notifyListeners();
return I; return I;
} }
@@ -159,6 +180,8 @@ class ArcaneReactiveTheme extends ArcaneService {
/// ``` /// ```
ArcaneReactiveTheme setLightTheme(ThemeData theme) { ArcaneReactiveTheme setLightTheme(ThemeData theme) {
_lightTheme.value = theme; _lightTheme.value = theme;
_themeStreamController.add(theme);
_currentTheme = theme;
notifyListeners(); notifyListeners();
return I; return I;
} }
@@ -173,12 +196,14 @@ class ArcaneReactiveTheme extends ArcaneService {
_lightTheme.value = ThemeData.light(); _lightTheme.value = ThemeData.light();
_followingSystemTheme = false; _followingSystemTheme = false;
_updateTheme(ThemeMode.light); _updateTheme(ThemeMode.light);
_themeStreamController.add(_lightTheme.value);
_currentTheme = _lightTheme.value;
notifyListeners(); notifyListeners();
} }
/// Updates the current theme mode and broadcasts the change. /// Updates the current theme mode and broadcasts the change.
void _updateTheme(ThemeMode themeMode) { void _updateTheme(ThemeMode themeMode) {
_currentTheme = themeMode; _currentThemeMode = themeMode;
_themeStreamController.add(themeMode); _themeModeStreamController.add(themeMode);
} }
} }
@@ -16,27 +16,33 @@ class ArcaneThemeSwitcher extends StatefulWidget {
} }
class _ArcaneThemeSwitcherState extends State<ArcaneThemeSwitcher> { class _ArcaneThemeSwitcherState extends State<ArcaneThemeSwitcher> {
late final StreamSubscription<ThemeMode> _subscription; late final StreamSubscription<ThemeMode> _themeModeSubscription;
late final StreamSubscription<ThemeData> _themeSubscription;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_subscription = ArcaneReactiveTheme.I.themeChanges.listen((_) { _themeModeSubscription = ArcaneReactiveTheme.I.themeModeChanges.listen((_) {
setState(() {});
});
_themeSubscription = ArcaneReactiveTheme.I.themeDataChanges.listen((_) {
setState(() {}); setState(() {});
}); });
} }
@override @override
void dispose() { void dispose() {
_subscription.cancel(); _themeModeSubscription.cancel();
_themeSubscription.cancel();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ArcaneTheme( return ArcaneTheme(
themeMode: ArcaneReactiveTheme.I.currentTheme, themeMode: ArcaneReactiveTheme.I.currentThemeMode,
followSystem: ArcaneReactiveTheme.I.isFollowingSystemTheme, followSystem: ArcaneReactiveTheme.I.isFollowingSystemTheme,
theme: ArcaneReactiveTheme.I.currentTheme,
child: widget.child, child: widget.child,
); );
} }
@@ -16,15 +16,15 @@ void main() {
group("theme mode", () { group("theme mode", () {
test("initial mode is light", () { test("initial mode is light", () {
expect(theme.currentTheme, equals(ThemeMode.light)); expect(theme.currentThemeMode, equals(ThemeMode.light));
}); });
test("switchTheme toggles between light and dark", () { test("switchTheme toggles between light and dark", () {
expect(theme.currentTheme, equals(ThemeMode.light)); expect(theme.currentThemeMode, equals(ThemeMode.light));
theme.switchTheme(); theme.switchTheme();
expect(theme.currentTheme, equals(ThemeMode.dark)); expect(theme.currentThemeMode, equals(ThemeMode.dark));
theme.switchTheme(); theme.switchTheme();
expect(theme.currentTheme, equals(ThemeMode.light)); expect(theme.currentThemeMode, equals(ThemeMode.light));
}); });
test("switching theme notifies listeners", () { test("switching theme notifies listeners", () {
@@ -66,7 +66,7 @@ void main() {
}); });
theme.addListener(() { theme.addListener(() {
currentTheme = theme.currentTheme; currentTheme = theme.currentThemeMode;
}); });
expect(currentTheme, ThemeMode.system); expect(currentTheme, ThemeMode.system);
@@ -106,7 +106,7 @@ void main() {
Arcane.theme.followSystemTheme(lightContext); Arcane.theme.followSystemTheme(lightContext);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(theme.currentTheme, equals(ThemeMode.light)); expect(theme.currentThemeMode, equals(ThemeMode.light));
await tester.pumpWidget( await tester.pumpWidget(
const MediaQuery( const MediaQuery(
@@ -121,7 +121,7 @@ void main() {
Arcane.theme.followSystemTheme(darkContext); Arcane.theme.followSystemTheme(darkContext);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(theme.currentTheme, equals(ThemeMode.dark)); expect(theme.currentThemeMode, equals(ThemeMode.dark));
}); });
}); });
}); });