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
+65 -53
View File
@@ -270,9 +270,6 @@ class ArcaneThemeExample extends StatelessWidget {
super.key,
});
static final Listenable themeListenable =
Listenable.merge([Arcane.theme.darkTheme, Arcane.theme.lightTheme]);
@override
Widget build(BuildContext context) {
return Card(
@@ -313,37 +310,40 @@ class ArcaneThemeExample extends StatelessWidget {
Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: Arcane.theme.isFollowingSystemTheme,
onChanged: (value) {
final ThemeMode oldTheme =
Arcane.theme.currentThemeMode;
if (value == true) {
Arcane.theme.followSystemTheme(context);
Arcane.log(
"Switching theme",
metadata: {
"followingSystemTheme":
"${Arcane.theme.isFollowingSystemTheme}",
"newMode": Arcane.theme.currentThemeMode.name,
"oldMode": oldTheme.name,
},
);
} else {
Arcane.theme.switchTheme(
themeMode: Arcane.theme.systemThemeMode,
);
Arcane.log(
"Switching theme",
metadata: {
"followingSystemTheme":
"${Arcane.theme.isFollowingSystemTheme}",
"newMode": Arcane.theme.currentThemeMode.name,
"oldMode": oldTheme.name,
},
);
}
},
ValueListenableBuilder<bool>(
valueListenable: Arcane.theme.followingSystemThemeChanges,
builder: (context, isFollowingSystem, _) => Checkbox(
value: isFollowingSystem,
onChanged: (value) {
final ThemeMode oldTheme =
Arcane.theme.currentThemeMode;
if (value == true) {
Arcane.theme.followSystemTheme(context);
Arcane.log(
"Switching theme",
metadata: {
"followingSystemTheme":
"${Arcane.theme.isFollowingSystemTheme}",
"newMode": Arcane.theme.currentThemeMode.name,
"oldMode": oldTheme.name,
},
);
} else {
Arcane.theme.switchTheme(
themeMode: Arcane.theme.systemThemeMode,
);
Arcane.log(
"Switching theme",
metadata: {
"followingSystemTheme":
"${Arcane.theme.isFollowingSystemTheme}",
"newMode": Arcane.theme.currentThemeMode.name,
"oldMode": oldTheme.name,
},
);
}
},
),
),
const Text("Follow system"),
],
@@ -359,8 +359,9 @@ class ArcaneThemeExample extends StatelessWidget {
const Text("Color"),
Expanded(
child: ValueListenableBuilder<ThemeData>(
valueListenable: Arcane.theme.themeDataChanges,
builder: (context, themeData, _) => ListView.separated(
valueListenable: Arcane.theme.effectiveThemeChanges,
builder: (context, effectiveTheme, _) =>
ListView.separated(
itemCount: colors.length,
scrollDirection: Axis.horizontal,
separatorBuilder: (_, __) => const SizedBox(width: 4),
@@ -387,23 +388,34 @@ class ArcaneThemeExample extends StatelessWidget {
"Setting ${Arcane.theme.currentThemeMode.name} theme color to ${colors[index].name}",
);
},
child: ValueListenableBuilder<ThemeMode>(
valueListenable: Arcane.theme.themeModeChanges,
builder: (context, themeMode, _) {
return Container(
key: Key("${colors[index]}-${themeMode}"),
decoration: BoxDecoration(
color: colors[index],
border:
themeData.colorScheme.primary.name ==
colors[index].name
? Border.all(width: 2)
: null,
),
width: 20,
height: 20,
);
},
child: Container(
key: Key(
"${colors[index]}-${Arcane.theme.currentThemeMode}"),
decoration: BoxDecoration(
color: colors[index],
border:
effectiveTheme.colorScheme.primary.name ==
colors[index].name
? 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,
),
width: 20,
height: 20,
),
);
},
@@ -66,6 +66,22 @@ class ArcaneReactiveTheme extends ArcaneService {
final ValueNotifier<ThemeData> _themeNotifier =
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
// ************************************************************************ //
@@ -101,6 +117,7 @@ class ArcaneReactiveTheme extends ArcaneService {
/// ```
ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) {
_followingSystemTheme = false;
_followingSystemThemeNotifier.value = false;
if (themeMode != null) {
_updateTheme(themeMode);
@@ -127,6 +144,7 @@ class ArcaneReactiveTheme extends ArcaneService {
/// ```
ArcaneReactiveTheme followSystemTheme(BuildContext context) {
_followingSystemTheme = true;
_followingSystemThemeNotifier.value = true;
_currentSystemThemeMode =
context.isDarkMode ? ThemeMode.dark : ThemeMode.light;
@@ -152,6 +170,7 @@ class ArcaneReactiveTheme extends ArcaneService {
if (_currentThemeMode == ThemeMode.dark) {
_themeNotifier.value = theme;
_currentTheme = theme;
_effectiveThemeNotifier.value = theme;
}
return I;
@@ -173,6 +192,7 @@ class ArcaneReactiveTheme extends ArcaneService {
if (_currentThemeMode == ThemeMode.light) {
_themeNotifier.value = theme;
_currentTheme = theme;
_effectiveThemeNotifier.value = theme;
}
return I;
@@ -187,9 +207,11 @@ class ArcaneReactiveTheme extends ArcaneService {
_darkTheme.value = ThemeData.dark();
_lightTheme.value = ThemeData.light();
_followingSystemTheme = false;
_followingSystemThemeNotifier.value = false;
_updateTheme(ThemeMode.light);
_themeNotifier.value = _lightTheme.value;
_currentTheme = _lightTheme.value;
_effectiveThemeNotifier.value = _lightTheme.value;
}
/// 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;
_currentTheme = newTheme;
_themeNotifier.value = newTheme;
_effectiveThemeNotifier.value = newTheme;
}
/// Disposes of the theme service resources.
@@ -212,6 +235,8 @@ class ArcaneReactiveTheme extends ArcaneService {
_systemThemeNotifier.dispose();
_themeModeNotifier.dispose();
_themeNotifier.dispose();
_effectiveThemeNotifier.dispose();
_followingSystemThemeNotifier.dispose();
_darkTheme.dispose();
_lightTheme.dispose();
super.dispose();
@@ -98,11 +98,16 @@ class MockArcaneAuthInterface extends _i1.Mock
as _i5.Future<void>);
@override
_i5.Future<_i2.Result<void, String>> logout() =>
_i5.Future<_i2.Result<void, String>> logout({
_i5.Future<void> Function()? onLoggedOut,
}) =>
(super.noSuchMethod(
Invocation.method(#logout, []),
Invocation.method(#logout, [], {#onLoggedOut: onLoggedOut}),
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>>);