Fixes tests and updates reactive theme

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2025-04-23 21:57:20 +02:00
parent cfd9052442
commit 23f0387389
5 changed files with 91 additions and 29 deletions
+2 -1
View File
@@ -59,7 +59,8 @@ class MainApp extends StatelessWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: Arcane.theme.light, theme: Arcane.theme.light,
darkTheme: Arcane.theme.dark, darkTheme: Arcane.theme.dark,
themeMode: Arcane.theme.currentTheme, themeMode:
ArcaneTheme.of(context)?.themeMode ?? Arcane.theme.currentTheme,
home: Scaffold( home: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Arcane Framework Example"), title: const Text("Arcane Framework Example"),
+1
View File
@@ -45,4 +45,5 @@ export "package:arcane_framework/src/services/authentication/authentication_serv
export "package:arcane_framework/src/services/feature_flags/feature_flags_service.dart"; export "package:arcane_framework/src/services/feature_flags/feature_flags_service.dart";
export "package:arcane_framework/src/services/logging/logging_service.dart"; export "package:arcane_framework/src/services/logging/logging_service.dart";
export "package:arcane_framework/src/services/reactive_theme/reactive_theme_service.dart"; export "package:arcane_framework/src/services/reactive_theme/reactive_theme_service.dart";
export "package:arcane_framework/src/services/reactive_theme/reactive_theme_wrapper.dart";
export "package:result_monad/result_monad.dart"; export "package:result_monad/result_monad.dart";
+16 -20
View File
@@ -56,16 +56,15 @@ class _ArcaneAppState extends State<ArcaneApp> with WidgetsBindingObserver {
serviceInstances: widget.services, serviceInstances: widget.services,
child: Builder( child: Builder(
key: _appKey, key: _appKey,
builder: (context) { builder: (BuildContext currentContext) {
_updateContextReference(context);
return StreamBuilder<ThemeMode>( return StreamBuilder<ThemeMode>(
stream: ArcaneReactiveTheme.I.currentThemeStream, stream: ArcaneReactiveTheme.I.currentThemeStream,
initialData: ArcaneReactiveTheme.I.currentTheme,
builder: (context, AsyncSnapshot<ThemeMode> snapshot) { builder: (context, AsyncSnapshot<ThemeMode> snapshot) {
if (!snapshot.hasData) return widget.child; final ThemeMode themeMode = snapshot.data ?? ThemeMode.light;
return KeyedSubtree( return ArcaneTheme(
key: Key(snapshot.data!.name), themeMode: themeMode,
child: widget.child, child: widget.child,
); );
}, },
@@ -76,17 +75,6 @@ class _ArcaneAppState extends State<ArcaneApp> with WidgetsBindingObserver {
); );
} }
// Update our context reference whenever the widget is built
void _updateContextReference(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// Only store this context if the widget is still mounted
if (mounted) {
// Store this context in a way that ArcaneReactiveTheme can access it
ArcaneReactiveTheme.I.checkSystemTheme(context);
}
});
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -103,9 +91,17 @@ class _ArcaneAppState extends State<ArcaneApp> with WidgetsBindingObserver {
@override @override
void didChangePlatformBrightness() { void didChangePlatformBrightness() {
// This is called when the system brightness changes // When system brightness changes, find the current builder context
// Check and update the theme if we're following system theme // and use it to check the system theme
ArcaneReactiveTheme.I.checkSystemTheme(context); if (mounted && _appKey.currentContext != null) {
// Use the current context from the key to check system theme
final BuildContext currentContext = _appKey.currentContext!;
if (ArcaneReactiveTheme.I.isFollowingSystemTheme) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ArcaneReactiveTheme.I.checkSystemTheme(currentContext);
});
}
}
super.didChangePlatformBrightness(); super.didChangePlatformBrightness();
} }
} }
@@ -10,7 +10,10 @@ part "reactive_theme_extensions.dart";
/// `ArcaneReactiveTheme` allows switching between light and dark themes and provides /// `ArcaneReactiveTheme` allows switching between light and dark themes and provides
/// methods to customize the themes. The current theme mode can be accessed, and the /// methods to customize the themes. The current theme mode can be accessed, and the
/// theme can be switched at runtime. /// theme can be switched at runtime.
class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver { ///
/// System theme changes are detected by the `ArcaneApp` widget, which ensures
/// theme updates happen automatically when the device theme changes.
class ArcaneReactiveTheme extends ArcaneService {
/// The singleton instance of `ArcaneReactiveTheme`. /// The singleton instance of `ArcaneReactiveTheme`.
static final ArcaneReactiveTheme _instance = ArcaneReactiveTheme._internal(); static final ArcaneReactiveTheme _instance = ArcaneReactiveTheme._internal();
@@ -22,6 +25,12 @@ class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver {
// Whether to follow system theme // Whether to follow system theme
bool _followingSystemTheme = false; bool _followingSystemTheme = false;
/// Whether the theme service is currently following the system theme.
///
/// When true, the theme will automatically switch between light and dark
/// based on the system's brightness setting.
bool get isFollowingSystemTheme => _followingSystemTheme;
final ValueNotifier<ThemeMode> _systemThemeNotifier = final ValueNotifier<ThemeMode> _systemThemeNotifier =
ValueNotifier(ThemeMode.system); ValueNotifier(ThemeMode.system);
@@ -32,10 +41,12 @@ class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver {
}, },
); );
/// Stream of theme mode changes that can be listened to for reactive UI updates.
Stream<ThemeMode> get currentThemeStream => I._themeStreamController.stream; Stream<ThemeMode> get currentThemeStream => I._themeStreamController.stream;
ThemeMode _currentTheme = ThemeMode.light; ThemeMode _currentTheme = ThemeMode.light;
/// The currently active theme mode (light or dark).
ThemeMode get currentTheme => _currentTheme; ThemeMode get currentTheme => _currentTheme;
/// A listenable that notifies listeners when the system theme mode changes. /// A listenable that notifies listeners when the system theme mode changes.
@@ -49,10 +60,14 @@ class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver {
/// Returns the current dark theme `ThemeData`. /// Returns the current dark theme `ThemeData`.
ThemeData get dark => _darkTheme.value; ThemeData get dark => _darkTheme.value;
/// ValueNotifier for the dark theme that can be observed for changes.
ValueNotifier<ThemeData> get darkTheme => I._darkTheme; ValueNotifier<ThemeData> get darkTheme => I._darkTheme;
/// Returns the current light theme `ThemeData`. /// Returns the current light theme `ThemeData`.
ThemeData get light => _lightTheme.value; ThemeData get light => _lightTheme.value;
/// ValueNotifier for the light theme that can be observed for changes.
ValueNotifier<ThemeData> get lightTheme => I._lightTheme; ValueNotifier<ThemeData> get lightTheme => I._lightTheme;
/// Switches the current theme between light and dark modes. /// Switches the current theme between light and dark modes.
@@ -82,24 +97,26 @@ class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver {
/// Switches the current theme between light and dark modes automatically /// Switches the current theme between light and dark modes automatically
/// based upon the system's current mode. /// based upon the system's current mode.
/// ///
/// This will also register for system theme changes, so the theme will
/// automatically update when the system theme changes.
///
/// Example: /// Example:
/// ```dart /// ```dart
/// ArcaneReactiveTheme.I.followSystemTheme(context); /// ArcaneReactiveTheme.I.followSystemTheme(context);
/// final ThemeMode mode = Arcane.theme.systemTheme.value;
/// ``` /// ```
ArcaneReactiveTheme followSystemTheme(BuildContext context) { ArcaneReactiveTheme followSystemTheme(BuildContext context) {
if (!_followingSystemTheme) {
_followingSystemTheme = true; _followingSystemTheme = true;
WidgetsBinding.instance.addPostFrameCallback((_) { // Always check the system theme when this method is called
checkSystemTheme(context); checkSystemTheme(context);
});
}
return I; return I;
} }
/// Check and apply the system theme if we're following it /// Check and apply the system theme if we're following it
///
/// This is called automatically when the system brightness changes if
/// [followSystemTheme] has been enabled.
void checkSystemTheme(BuildContext context) { void checkSystemTheme(BuildContext context) {
if (!_followingSystemTheme) return; if (!_followingSystemTheme) return;
@@ -109,6 +126,7 @@ class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver {
final ThemeMode systemMode = final ThemeMode systemMode =
systemBrightness == Brightness.dark ? ThemeMode.dark : ThemeMode.light; systemBrightness == Brightness.dark ? ThemeMode.dark : ThemeMode.light;
// Only update and notify if the theme actually changed
if (systemMode != _currentTheme) { if (systemMode != _currentTheme) {
_updateTheme(systemMode); _updateTheme(systemMode);
notifyListeners(); notifyListeners();
@@ -145,6 +163,10 @@ class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver {
return I; return I;
} }
/// Resets the theme service to its default state.
///
/// This resets both light and dark themes to their default values and
/// disables system theme following.
@visibleForTesting @visibleForTesting
void reset() { void reset() {
_darkTheme.value = ThemeData.dark(); _darkTheme.value = ThemeData.dark();
@@ -154,6 +176,7 @@ class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver {
notifyListeners(); notifyListeners();
} }
/// Updates the current theme mode and broadcasts the change.
void _updateTheme(ThemeMode themeMode) { void _updateTheme(ThemeMode themeMode) {
_currentTheme = themeMode; _currentTheme = themeMode;
_themeStreamController.add(themeMode); _themeStreamController.add(themeMode);
@@ -0,0 +1,41 @@
import "package:arcane_framework/src/services/reactive_theme/reactive_theme_service.dart";
import "package:flutter/material.dart";
class ArcaneTheme extends InheritedWidget {
final ThemeMode themeMode;
const ArcaneTheme({
required this.themeMode,
required super.child,
super.key,
});
static ArcaneTheme? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ArcaneTheme>();
}
@override
bool updateShouldNotify(ArcaneTheme oldWidget) {
return oldWidget.themeMode != themeMode;
}
static ArcaneReactiveTheme get service => ArcaneReactiveTheme.I;
static bool get isFollowingSystemTheme => service.isFollowingSystemTheme;
static Stream<ThemeMode> get currentThemeStream => service.currentThemeStream;
static ThemeMode get currentTheme => service.currentTheme;
static ThemeMode get systemTheme => service.systemTheme;
static ThemeData get dark => service.dark;
static ValueNotifier<ThemeData> get darkTheme => service.darkTheme;
static ThemeData get light => service.light;
static ValueNotifier<ThemeData> get lightTheme => service.lightTheme;
static ArcaneReactiveTheme Function({ThemeMode? themeMode}) get switchTheme =>
service.switchTheme;
static ArcaneReactiveTheme Function(BuildContext context)
get followSystemTheme => service.followSystemTheme;
static void get checkSystemTheme => service.checkSystemTheme;
static ArcaneReactiveTheme Function(ThemeData theme) get setDarkTheme =>
service.setDarkTheme;
static ArcaneReactiveTheme Function(ThemeData theme) get setLightTheme =>
service.setLightTheme;
}