mirror of
https://github.com/hanskokx/arcane_framework.git
synced 2026-05-14 10:29:06 +02:00
Refactor theme management to use ValueNotifier for reactive updates
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
+9
-10
@@ -358,9 +358,9 @@ class ArcaneThemeExample extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Text("Color"),
|
const Text("Color"),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: StreamBuilder(
|
child: ValueListenableBuilder<ThemeData>(
|
||||||
stream: Arcane.theme.themeDataChanges,
|
valueListenable: Arcane.theme.themeDataChanges,
|
||||||
builder: (context, themeData) => ListView.separated(
|
builder: (context, themeData, _) => 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,16 +387,15 @@ 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: StreamBuilder<ThemeMode>(
|
child: ValueListenableBuilder<ThemeMode>(
|
||||||
stream: Arcane.theme.themeModeChanges,
|
valueListenable: Arcane.theme.themeModeChanges,
|
||||||
builder: (context, themeMode) {
|
builder: (context, themeMode, _) {
|
||||||
return Container(
|
return Container(
|
||||||
key:
|
key: Key("${colors[index]}-${themeMode}"),
|
||||||
Key("${colors[index]}-${themeMode.data}"),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colors[index],
|
color: colors[index],
|
||||||
border: themeData.data?.colorScheme.primary
|
border:
|
||||||
.name ==
|
themeData.colorScheme.primary.name ==
|
||||||
colors[index].name
|
colors[index].name
|
||||||
? Border.all(width: 2)
|
? Border.all(width: 2)
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import "dart:async";
|
|
||||||
|
|
||||||
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";
|
||||||
@@ -34,12 +33,8 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
/// Tracks the current system theme mode
|
/// Tracks the current system theme mode
|
||||||
ThemeMode _currentSystemThemeMode = ThemeMode.system;
|
ThemeMode _currentSystemThemeMode = ThemeMode.system;
|
||||||
|
|
||||||
final StreamController<ThemeMode> _systemStreamController =
|
final ValueNotifier<ThemeMode> _systemThemeNotifier =
|
||||||
StreamController<ThemeMode>.broadcast(
|
ValueNotifier<ThemeMode>(ThemeMode.system);
|
||||||
onCancel: () {
|
|
||||||
I._systemStreamController.close();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// ************************************************************************ //
|
// ************************************************************************ //
|
||||||
// * MARK: ThemeMode
|
// * MARK: ThemeMode
|
||||||
@@ -52,32 +47,24 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
ThemeMode get currentThemeMode => _currentThemeMode;
|
ThemeMode get currentThemeMode => _currentThemeMode;
|
||||||
ThemeMode _currentThemeMode = ThemeMode.light;
|
ThemeMode _currentThemeMode = ThemeMode.light;
|
||||||
|
|
||||||
/// Stream of `ThemeMode` changes that can be listened to for reactive UI updates.
|
/// ValueListenable of `ThemeMode` changes that can be listened to for reactive UI updates.
|
||||||
Stream<ThemeMode> get themeModeChanges => I._themeModeStreamController.stream;
|
ValueListenable<ThemeMode> get themeModeChanges => I._themeModeNotifier;
|
||||||
|
|
||||||
final StreamController<ThemeMode> _themeModeStreamController =
|
final ValueNotifier<ThemeMode> _themeModeNotifier =
|
||||||
StreamController<ThemeMode>.broadcast(
|
ValueNotifier<ThemeMode>(ThemeMode.light);
|
||||||
onCancel: () {
|
|
||||||
I._themeModeStreamController.close();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// ************************************************************************ //
|
// ************************************************************************ //
|
||||||
// * MARK: ThemeData
|
// * MARK: ThemeData
|
||||||
// ************************************************************************ //
|
// ************************************************************************ //
|
||||||
/// The currently active theme style.
|
/// The currently active theme style.
|
||||||
ThemeData get currentTheme => _currentTheme;
|
ThemeData get currentTheme => _currentTheme;
|
||||||
ThemeData _currentTheme = ThemeData();
|
ThemeData _currentTheme = ThemeData.light();
|
||||||
|
|
||||||
/// Stream of `ThemeData` changes that can be listened to for reactive UI updates.
|
/// ValueListenable of `ThemeData` changes that can be listened to for reactive UI updates.
|
||||||
Stream<ThemeData> get themeDataChanges => I._themeStreamController.stream;
|
ValueListenable<ThemeData> get themeDataChanges => I._themeNotifier;
|
||||||
|
|
||||||
final StreamController<ThemeData> _themeStreamController =
|
final ValueNotifier<ThemeData> _themeNotifier =
|
||||||
StreamController<ThemeData>.broadcast(
|
ValueNotifier<ThemeData>(ThemeData.light());
|
||||||
onCancel: () {
|
|
||||||
I._themeStreamController.close();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// ************************************************************************ //
|
// ************************************************************************ //
|
||||||
// * MARK: Light/Dark theme
|
// * MARK: Light/Dark theme
|
||||||
@@ -143,13 +130,9 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
|
|
||||||
_currentSystemThemeMode =
|
_currentSystemThemeMode =
|
||||||
context.isDarkMode ? ThemeMode.dark : ThemeMode.light;
|
context.isDarkMode ? ThemeMode.dark : ThemeMode.light;
|
||||||
_systemStreamController.add(_currentSystemThemeMode);
|
_systemThemeNotifier.value = _currentSystemThemeMode;
|
||||||
_updateTheme(_currentSystemThemeMode);
|
_updateTheme(_currentSystemThemeMode);
|
||||||
|
|
||||||
final ThemeData theme = systemThemeMode == ThemeMode.dark ? dark : light;
|
|
||||||
_themeStreamController.add(theme);
|
|
||||||
_currentTheme = theme;
|
|
||||||
|
|
||||||
return I;
|
return I;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,8 +147,12 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
/// ```
|
/// ```
|
||||||
ArcaneReactiveTheme setDarkTheme(ThemeData theme) {
|
ArcaneReactiveTheme setDarkTheme(ThemeData theme) {
|
||||||
_darkTheme.value = theme;
|
_darkTheme.value = theme;
|
||||||
_themeStreamController.add(theme);
|
|
||||||
|
// Only update current theme if we're currently in dark mode
|
||||||
|
if (_currentThemeMode == ThemeMode.dark) {
|
||||||
|
_themeNotifier.value = theme;
|
||||||
_currentTheme = theme;
|
_currentTheme = theme;
|
||||||
|
}
|
||||||
|
|
||||||
return I;
|
return I;
|
||||||
}
|
}
|
||||||
@@ -181,8 +168,12 @@ class ArcaneReactiveTheme extends ArcaneService {
|
|||||||
/// ```
|
/// ```
|
||||||
ArcaneReactiveTheme setLightTheme(ThemeData theme) {
|
ArcaneReactiveTheme setLightTheme(ThemeData theme) {
|
||||||
_lightTheme.value = theme;
|
_lightTheme.value = theme;
|
||||||
_themeStreamController.add(theme);
|
|
||||||
|
// Only update current theme if we're currently in light mode
|
||||||
|
if (_currentThemeMode == ThemeMode.light) {
|
||||||
|
_themeNotifier.value = theme;
|
||||||
_currentTheme = theme;
|
_currentTheme = theme;
|
||||||
|
}
|
||||||
|
|
||||||
return I;
|
return I;
|
||||||
}
|
}
|
||||||
@@ -197,13 +188,32 @@ 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);
|
_themeNotifier.value = _lightTheme.value;
|
||||||
_currentTheme = _lightTheme.value;
|
_currentTheme = _lightTheme.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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) {
|
||||||
_currentThemeMode = themeMode;
|
_currentThemeMode = themeMode;
|
||||||
_themeModeStreamController.add(themeMode);
|
_themeModeNotifier.value = themeMode;
|
||||||
|
|
||||||
|
// Update the current theme data based on the theme mode
|
||||||
|
final ThemeData newTheme = themeMode == ThemeMode.dark ? dark : light;
|
||||||
|
_currentTheme = newTheme;
|
||||||
|
_themeNotifier.value = newTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disposes of the theme service resources.
|
||||||
|
///
|
||||||
|
/// This method should be called when the service is no longer needed
|
||||||
|
/// to clean up ValueNotifiers and prevent memory leaks.
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_systemThemeNotifier.dispose();
|
||||||
|
_themeModeNotifier.dispose();
|
||||||
|
_themeNotifier.dispose();
|
||||||
|
_darkTheme.dispose();
|
||||||
|
_lightTheme.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import "dart:async";
|
|
||||||
|
|
||||||
import "package:arcane_framework/arcane_framework.dart";
|
import "package:arcane_framework/arcane_framework.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
@@ -17,27 +15,15 @@ class ArcaneThemeSwitcher extends StatefulWidget {
|
|||||||
|
|
||||||
class _ArcaneThemeSwitcherState extends State<ArcaneThemeSwitcher>
|
class _ArcaneThemeSwitcherState extends State<ArcaneThemeSwitcher>
|
||||||
with WidgetsBindingObserver {
|
with WidgetsBindingObserver {
|
||||||
late final StreamSubscription<ThemeMode> _themeModeSubscription;
|
|
||||||
late final StreamSubscription<ThemeData> _themeSubscription;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Register as an observer to detect system theme changes
|
// Register as an observer to detect system theme changes
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
_themeModeSubscription = ArcaneReactiveTheme.I.themeModeChanges.listen((_) {
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
_themeSubscription = ArcaneReactiveTheme.I.themeDataChanges.listen((_) {
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_themeModeSubscription.cancel();
|
|
||||||
_themeSubscription.cancel();
|
|
||||||
// Clean up the observer when the widget is disposed
|
// Clean up the observer when the widget is disposed
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -45,12 +31,22 @@ class _ArcaneThemeSwitcherState extends State<ArcaneThemeSwitcher>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<ThemeMode>(
|
||||||
|
valueListenable: ArcaneReactiveTheme.I.themeModeChanges,
|
||||||
|
builder: (BuildContext context, ThemeMode themeMode, Widget? child) {
|
||||||
|
return ValueListenableBuilder<ThemeData>(
|
||||||
|
valueListenable: ArcaneReactiveTheme.I.themeDataChanges,
|
||||||
|
builder: (BuildContext context, ThemeData themeData, Widget? child) {
|
||||||
return _ArcaneTheme(
|
return _ArcaneTheme(
|
||||||
themeMode: ArcaneReactiveTheme.I.currentThemeMode,
|
themeMode: themeMode,
|
||||||
followSystem: ArcaneReactiveTheme.I.isFollowingSystemTheme,
|
followSystem: ArcaneReactiveTheme.I.isFollowingSystemTheme,
|
||||||
theme: ArcaneReactiveTheme.I.currentTheme,
|
theme: themeData,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ void main() {
|
|||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
theme = ArcaneReactiveTheme.I;
|
theme = ArcaneReactiveTheme.I;
|
||||||
|
theme.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
theme.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("singleton instance is consistent", () {
|
test("singleton instance is consistent", () {
|
||||||
@@ -29,7 +34,7 @@ void main() {
|
|||||||
|
|
||||||
test("switching theme notifies listeners", () {
|
test("switching theme notifies listeners", () {
|
||||||
var notified = false;
|
var notified = false;
|
||||||
theme.addListener(() => notified = true);
|
theme.themeModeChanges.addListener(() => notified = true);
|
||||||
theme.switchTheme();
|
theme.switchTheme();
|
||||||
expect(notified, true);
|
expect(notified, true);
|
||||||
});
|
});
|
||||||
@@ -55,7 +60,7 @@ void main() {
|
|||||||
test("theme updates notify listeners", () {
|
test("theme updates notify listeners", () {
|
||||||
bool darkNotified = false;
|
bool darkNotified = false;
|
||||||
bool lightNotified = false;
|
bool lightNotified = false;
|
||||||
ThemeMode currentTheme = ThemeMode.system;
|
ThemeMode currentTheme = theme.currentThemeMode;
|
||||||
|
|
||||||
theme.darkTheme.addListener(() {
|
theme.darkTheme.addListener(() {
|
||||||
darkNotified = true;
|
darkNotified = true;
|
||||||
@@ -65,29 +70,41 @@ void main() {
|
|||||||
lightNotified = true;
|
lightNotified = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
theme.addListener(() {
|
theme.themeModeChanges.addListener(() {
|
||||||
currentTheme = theme.currentThemeMode;
|
currentTheme = theme.currentThemeMode;
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(currentTheme, ThemeMode.system);
|
expect(currentTheme, ThemeMode.light);
|
||||||
|
|
||||||
theme.setDarkTheme(ThemeData.dark());
|
// Use custom themes to ensure ValueNotifier detects changes
|
||||||
theme.setLightTheme(ThemeData.light());
|
final customDarkTheme = ThemeData.dark().copyWith(
|
||||||
|
primaryColor: Colors.purple,
|
||||||
|
);
|
||||||
|
final customLightTheme = ThemeData.light().copyWith(
|
||||||
|
primaryColor: Colors.orange,
|
||||||
|
);
|
||||||
|
|
||||||
|
theme.setDarkTheme(customDarkTheme);
|
||||||
|
theme.setLightTheme(customLightTheme);
|
||||||
|
|
||||||
expect(darkNotified, true);
|
expect(darkNotified, true);
|
||||||
expect(lightNotified, true);
|
expect(lightNotified, true);
|
||||||
|
|
||||||
theme.switchTheme();
|
theme.switchTheme();
|
||||||
expect(currentTheme, ThemeMode.light);
|
expect(currentTheme, ThemeMode.dark);
|
||||||
|
|
||||||
theme.switchTheme();
|
theme.switchTheme();
|
||||||
expect(currentTheme, ThemeMode.dark);
|
expect(currentTheme, ThemeMode.light);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group("system theme following", () {
|
group("system theme following", () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
Arcane.theme.reset();
|
theme.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
theme.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets("followSystemTheme updates theme based on context brightness",
|
testWidgets("followSystemTheme updates theme based on context brightness",
|
||||||
|
|||||||
Reference in New Issue
Block a user