Refactor theme management to use ValueListenableBuilder for effective theme updates

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2025-07-01 16:56:30 +02:00
parent f8dcaf3c6c
commit b0c9fe7b98
3 changed files with 98 additions and 56 deletions
+28 -16
View File
@@ -270,9 +270,6 @@ class ArcaneThemeExample extends StatelessWidget {
super.key, super.key,
}); });
static final Listenable themeListenable =
Listenable.merge([Arcane.theme.darkTheme, Arcane.theme.lightTheme]);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
@@ -313,8 +310,10 @@ class ArcaneThemeExample extends StatelessWidget {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Checkbox( ValueListenableBuilder<bool>(
value: Arcane.theme.isFollowingSystemTheme, valueListenable: Arcane.theme.followingSystemThemeChanges,
builder: (context, isFollowingSystem, _) => Checkbox(
value: isFollowingSystem,
onChanged: (value) { onChanged: (value) {
final ThemeMode oldTheme = final ThemeMode oldTheme =
Arcane.theme.currentThemeMode; Arcane.theme.currentThemeMode;
@@ -345,6 +344,7 @@ class ArcaneThemeExample extends StatelessWidget {
} }
}, },
), ),
),
const Text("Follow system"), const Text("Follow system"),
], ],
), ),
@@ -359,8 +359,9 @@ class ArcaneThemeExample extends StatelessWidget {
const Text("Color"), const Text("Color"),
Expanded( Expanded(
child: ValueListenableBuilder<ThemeData>( child: ValueListenableBuilder<ThemeData>(
valueListenable: Arcane.theme.themeDataChanges, valueListenable: Arcane.theme.effectiveThemeChanges,
builder: (context, themeData, _) => ListView.separated( builder: (context, effectiveTheme, _) =>
ListView.separated(
itemCount: colors.length, itemCount: colors.length,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
separatorBuilder: (_, __) => const SizedBox(width: 4), separatorBuilder: (_, __) => const SizedBox(width: 4),
@@ -387,23 +388,34 @@ class ArcaneThemeExample extends StatelessWidget {
"Setting ${Arcane.theme.currentThemeMode.name} theme color to ${colors[index].name}", "Setting ${Arcane.theme.currentThemeMode.name} theme color to ${colors[index].name}",
); );
}, },
child: ValueListenableBuilder<ThemeMode>( child: Container(
valueListenable: Arcane.theme.themeModeChanges, key: Key(
builder: (context, themeMode, _) { "${colors[index]}-${Arcane.theme.currentThemeMode}"),
return Container(
key: Key("${colors[index]}-${themeMode}"),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors[index], color: colors[index],
border: border:
themeData.colorScheme.primary.name == effectiveTheme.colorScheme.primary.name ==
colors[index].name colors[index].name
? Border.all(width: 2) ? Border.all(
width: 2,
color: Colors.white,
)
: null,
boxShadow:
effectiveTheme.colorScheme.primary.name ==
colors[index].name
? [
const BoxShadow(
color: Colors.black,
spreadRadius: 1,
blurRadius: 1,
offset: Offset(0, 0),
),
]
: null, : null,
), ),
width: 20, width: 20,
height: 20, height: 20,
);
},
), ),
); );
}, },
@@ -66,6 +66,22 @@ class ArcaneReactiveTheme extends ArcaneService {
final ValueNotifier<ThemeData> _themeNotifier = final ValueNotifier<ThemeData> _themeNotifier =
ValueNotifier<ThemeData>(ThemeData.light()); ValueNotifier<ThemeData>(ThemeData.light());
/// ValueListenable that rebuilds when the effective theme changes.
/// This includes theme mode changes and active theme data changes.
/// Use this for most UI components that need to react to theme changes.
ValueListenable<ThemeData> get effectiveThemeChanges =>
I._effectiveThemeNotifier;
final ValueNotifier<ThemeData> _effectiveThemeNotifier =
ValueNotifier<ThemeData>(ThemeData.light());
/// ValueListenable that notifies when the system theme following state changes.
ValueListenable<bool> get followingSystemThemeChanges =>
I._followingSystemThemeNotifier;
final ValueNotifier<bool> _followingSystemThemeNotifier =
ValueNotifier<bool>(false);
// ************************************************************************ // // ************************************************************************ //
// * MARK: Light/Dark theme // * MARK: Light/Dark theme
// ************************************************************************ // // ************************************************************************ //
@@ -101,6 +117,7 @@ class ArcaneReactiveTheme extends ArcaneService {
/// ``` /// ```
ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) { ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) {
_followingSystemTheme = false; _followingSystemTheme = false;
_followingSystemThemeNotifier.value = false;
if (themeMode != null) { if (themeMode != null) {
_updateTheme(themeMode); _updateTheme(themeMode);
@@ -127,6 +144,7 @@ class ArcaneReactiveTheme extends ArcaneService {
/// ``` /// ```
ArcaneReactiveTheme followSystemTheme(BuildContext context) { ArcaneReactiveTheme followSystemTheme(BuildContext context) {
_followingSystemTheme = true; _followingSystemTheme = true;
_followingSystemThemeNotifier.value = true;
_currentSystemThemeMode = _currentSystemThemeMode =
context.isDarkMode ? ThemeMode.dark : ThemeMode.light; context.isDarkMode ? ThemeMode.dark : ThemeMode.light;
@@ -152,6 +170,7 @@ class ArcaneReactiveTheme extends ArcaneService {
if (_currentThemeMode == ThemeMode.dark) { if (_currentThemeMode == ThemeMode.dark) {
_themeNotifier.value = theme; _themeNotifier.value = theme;
_currentTheme = theme; _currentTheme = theme;
_effectiveThemeNotifier.value = theme;
} }
return I; return I;
@@ -173,6 +192,7 @@ class ArcaneReactiveTheme extends ArcaneService {
if (_currentThemeMode == ThemeMode.light) { if (_currentThemeMode == ThemeMode.light) {
_themeNotifier.value = theme; _themeNotifier.value = theme;
_currentTheme = theme; _currentTheme = theme;
_effectiveThemeNotifier.value = theme;
} }
return I; return I;
@@ -187,9 +207,11 @@ class ArcaneReactiveTheme extends ArcaneService {
_darkTheme.value = ThemeData.dark(); _darkTheme.value = ThemeData.dark();
_lightTheme.value = ThemeData.light(); _lightTheme.value = ThemeData.light();
_followingSystemTheme = false; _followingSystemTheme = false;
_followingSystemThemeNotifier.value = false;
_updateTheme(ThemeMode.light); _updateTheme(ThemeMode.light);
_themeNotifier.value = _lightTheme.value; _themeNotifier.value = _lightTheme.value;
_currentTheme = _lightTheme.value; _currentTheme = _lightTheme.value;
_effectiveThemeNotifier.value = _lightTheme.value;
} }
/// Updates the current theme mode and broadcasts the change. /// Updates the current theme mode and broadcasts the change.
@@ -201,6 +223,7 @@ class ArcaneReactiveTheme extends ArcaneService {
final ThemeData newTheme = themeMode == ThemeMode.dark ? dark : light; final ThemeData newTheme = themeMode == ThemeMode.dark ? dark : light;
_currentTheme = newTheme; _currentTheme = newTheme;
_themeNotifier.value = newTheme; _themeNotifier.value = newTheme;
_effectiveThemeNotifier.value = newTheme;
} }
/// Disposes of the theme service resources. /// Disposes of the theme service resources.
@@ -212,6 +235,8 @@ class ArcaneReactiveTheme extends ArcaneService {
_systemThemeNotifier.dispose(); _systemThemeNotifier.dispose();
_themeModeNotifier.dispose(); _themeModeNotifier.dispose();
_themeNotifier.dispose(); _themeNotifier.dispose();
_effectiveThemeNotifier.dispose();
_followingSystemThemeNotifier.dispose();
_darkTheme.dispose(); _darkTheme.dispose();
_lightTheme.dispose(); _lightTheme.dispose();
super.dispose(); super.dispose();
@@ -98,11 +98,16 @@ class MockArcaneAuthInterface extends _i1.Mock
as _i5.Future<void>); as _i5.Future<void>);
@override @override
_i5.Future<_i2.Result<void, String>> logout() => _i5.Future<_i2.Result<void, String>> logout({
_i5.Future<void> Function()? onLoggedOut,
}) =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.method(#logout, []), Invocation.method(#logout, [], {#onLoggedOut: onLoggedOut}),
returnValue: _i5.Future<_i2.Result<void, String>>.value( returnValue: _i5.Future<_i2.Result<void, String>>.value(
_FakeResult_0<void, String>(this, Invocation.method(#logout, [])), _FakeResult_0<void, String>(
this,
Invocation.method(#logout, [], {#onLoggedOut: onLoggedOut}),
),
), ),
) )
as _i5.Future<_i2.Result<void, String>>); as _i5.Future<_i2.Result<void, String>>);