Fixed theme

- fixed: currentMode, dark, light now actually emit new values when changed
- added: getters for lightTheme, darkTheme, and systemTheme
- breaking: currentMode -> systemTheme
- added: currentTheme
- breaking: currentMode => currentTheme
- change: Arcane.theme.followSystemTheme(context) required to follow system theme
- change: After following the system theme, calling Arcane.theme.switchTheme() cancels following the system theme
- added: ArcaneReactiveTheme is now registered in ArcaneServiceProvider when using ArcaneApp

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2025-04-18 18:15:05 +02:00
parent e9ba80bcc2
commit 2c8596d517
4 changed files with 43 additions and 24 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ class _MainAppState extends State<MainApp> {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: Arcane.theme.light, theme: Arcane.theme.light,
darkTheme: Arcane.theme.dark, darkTheme: Arcane.theme.dark,
themeMode: Arcane.theme.currentMode, themeMode: Arcane.theme.currentTheme,
home: Scaffold( home: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Arcane Framework Example"), title: const Text("Arcane Framework Example"),
+4 -1
View File
@@ -46,7 +46,10 @@ class ArcaneApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ArcaneEnvironmentProvider( return ArcaneEnvironmentProvider(
child: ArcaneServiceProvider( child: ArcaneServiceProvider(
serviceInstances: services, serviceInstances: [
ArcaneReactiveTheme.I,
...services,
],
child: child, child: child,
), ),
); );
@@ -1,5 +1,4 @@
import "package:arcane_framework/arcane_framework.dart"; import "package:arcane_framework/arcane_framework.dart";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
part "reactive_theme_extensions.dart"; part "reactive_theme_extensions.dart";
@@ -21,10 +20,13 @@ class ArcaneReactiveTheme extends ArcaneService {
final ValueNotifier<ThemeMode> _systemThemeNotifier = final ValueNotifier<ThemeMode> _systemThemeNotifier =
ValueNotifier(ThemeMode.light); ValueNotifier(ThemeMode.light);
/// Returns the current theme mode based on `_isDark`. final ValueNotifier<ThemeMode> _currentThemeNotifier =
/// ValueNotifier(ThemeMode.light);
/// If `_isDark` is true, it returns `ThemeMode.dark`, otherwise it returns `ThemeMode.light`.
ThemeMode get currentMode => I._systemThemeNotifier.value; ThemeMode get currentTheme => I._currentThemeNotifier.value;
/// A listenable that notifies listeners when the system theme mode changes.
ThemeMode get systemTheme => I._systemThemeNotifier.value;
/// 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());
@@ -40,9 +42,6 @@ class ArcaneReactiveTheme extends ArcaneService {
ThemeData get light => _lightTheme.value; ThemeData get light => _lightTheme.value;
ValueNotifier<ThemeData> get lightTheme => I._lightTheme; ValueNotifier<ThemeData> get lightTheme => I._lightTheme;
/// A listenable that notifies listeners when the syste theme mode changes.
ValueListenable<ThemeMode> get systemTheme => I._systemThemeNotifier;
/// Switches the current theme between light and dark modes. /// Switches the current theme between light and dark modes.
/// ///
/// If the theme is currently light, it switches to dark, and vice versa. It also /// If the theme is currently light, it switches to dark, and vice versa. It also
@@ -53,10 +52,15 @@ class ArcaneReactiveTheme extends ArcaneService {
/// ArcaneReactiveTheme.I.switchTheme(); /// ArcaneReactiveTheme.I.switchTheme();
/// ``` /// ```
ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) { ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) {
if (I._systemThemeNotifier.hasListeners) {
_systemThemeNotifier.removeListener(_systemThemeListener);
}
if (themeMode != null) { if (themeMode != null) {
_systemThemeNotifier.value = themeMode; _currentThemeNotifier.value = themeMode;
} else { } else {
_systemThemeNotifier.value = _systemThemeNotifier.value == ThemeMode.light _currentThemeNotifier.value =
_currentThemeNotifier.value == ThemeMode.light
? ThemeMode.dark ? ThemeMode.dark
: ThemeMode.light; : ThemeMode.light;
} }
@@ -78,8 +82,13 @@ class ArcaneReactiveTheme extends ArcaneService {
final ThemeMode systemMode = final ThemeMode systemMode =
context.isDarkMode ? ThemeMode.dark : ThemeMode.light; context.isDarkMode ? ThemeMode.dark : ThemeMode.light;
if (currentMode != systemMode) { if (!I._systemThemeNotifier.hasListeners) {
switchTheme(); I._systemThemeNotifier.addListener(_systemThemeListener);
}
if (systemMode != currentTheme) {
_systemThemeNotifier.value = systemMode;
notifyListeners();
} }
return I; return I;
@@ -120,6 +129,14 @@ class ArcaneReactiveTheme extends ArcaneService {
_darkTheme.value = ThemeData.dark(); _darkTheme.value = ThemeData.dark();
_lightTheme.value = ThemeData.light(); _lightTheme.value = ThemeData.light();
_systemThemeNotifier.value = ThemeMode.light; _systemThemeNotifier.value = ThemeMode.light;
_currentThemeNotifier.value = ThemeMode.light;
notifyListeners();
}
void _systemThemeListener() {
if (currentTheme != _systemThemeNotifier.value) {
_currentThemeNotifier.value = _systemThemeNotifier.value;
notifyListeners(); notifyListeners();
} }
} }
}
@@ -16,15 +16,15 @@ void main() {
group("theme mode", () { group("theme mode", () {
test("initial mode is light", () { test("initial mode is light", () {
expect(theme.currentMode, equals(ThemeMode.light)); expect(theme.currentTheme, equals(ThemeMode.light));
}); });
test("switchTheme toggles between light and dark", () { test("switchTheme toggles between light and dark", () {
expect(theme.currentMode, equals(ThemeMode.light)); expect(theme.currentTheme, equals(ThemeMode.light));
theme.switchTheme(); theme.switchTheme();
expect(theme.currentMode, equals(ThemeMode.dark)); expect(theme.currentTheme, equals(ThemeMode.dark));
theme.switchTheme(); theme.switchTheme();
expect(theme.currentMode, equals(ThemeMode.light)); expect(theme.currentTheme, 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.currentMode; currentTheme = theme.currentTheme;
}); });
expect(currentTheme, ThemeMode.system); expect(currentTheme, ThemeMode.system);
@@ -103,7 +103,7 @@ void main() {
final BuildContext lightContext = tester.element(find.byType(SizedBox)); final BuildContext lightContext = tester.element(find.byType(SizedBox));
theme.followSystemTheme(lightContext); theme.followSystemTheme(lightContext);
expect(theme.currentMode, equals(ThemeMode.light)); expect(theme.currentTheme, equals(ThemeMode.light));
await tester.pumpWidget( await tester.pumpWidget(
const MediaQuery( const MediaQuery(
@@ -113,9 +113,8 @@ void main() {
); );
final BuildContext darkContext = tester.element(find.byType(SizedBox)); final BuildContext darkContext = tester.element(find.byType(SizedBox));
theme.followSystemTheme(darkContext); theme.followSystemTheme(darkContext);
expect(theme.currentMode, equals(ThemeMode.dark)); expect(theme.currentTheme, equals(ThemeMode.dark));
}); });
testWidgets("followSystemTheme only switches when needed", testWidgets("followSystemTheme only switches when needed",