diff --git a/example/lib/main.dart b/example/lib/main.dart index 2738f17..cba5685 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -59,7 +59,8 @@ class MainApp extends StatelessWidget { debugShowCheckedModeBanner: false, theme: Arcane.theme.light, darkTheme: Arcane.theme.dark, - themeMode: Arcane.theme.currentTheme, + themeMode: + ArcaneTheme.of(context)?.themeMode ?? Arcane.theme.currentTheme, home: Scaffold( appBar: AppBar( title: const Text("Arcane Framework Example"), diff --git a/lib/arcane_framework.dart b/lib/arcane_framework.dart index 855f01a..a8f02b9 100644 --- a/lib/arcane_framework.dart +++ b/lib/arcane_framework.dart @@ -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/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_wrapper.dart"; export "package:result_monad/result_monad.dart"; diff --git a/lib/src/arcane_app.dart b/lib/src/arcane_app.dart index b62ef5f..d9daec5 100644 --- a/lib/src/arcane_app.dart +++ b/lib/src/arcane_app.dart @@ -56,16 +56,15 @@ class _ArcaneAppState extends State with WidgetsBindingObserver { serviceInstances: widget.services, child: Builder( key: _appKey, - builder: (context) { - _updateContextReference(context); - + builder: (BuildContext currentContext) { return StreamBuilder( stream: ArcaneReactiveTheme.I.currentThemeStream, + initialData: ArcaneReactiveTheme.I.currentTheme, builder: (context, AsyncSnapshot snapshot) { - if (!snapshot.hasData) return widget.child; + final ThemeMode themeMode = snapshot.data ?? ThemeMode.light; - return KeyedSubtree( - key: Key(snapshot.data!.name), + return ArcaneTheme( + themeMode: themeMode, child: widget.child, ); }, @@ -76,17 +75,6 @@ class _ArcaneAppState extends State 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 void initState() { super.initState(); @@ -103,9 +91,17 @@ class _ArcaneAppState extends State with WidgetsBindingObserver { @override void didChangePlatformBrightness() { - // This is called when the system brightness changes - // Check and update the theme if we're following system theme - ArcaneReactiveTheme.I.checkSystemTheme(context); + // When system brightness changes, find the current builder context + // and use it to check the system theme + 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(); } } diff --git a/lib/src/services/reactive_theme/reactive_theme_service.dart b/lib/src/services/reactive_theme/reactive_theme_service.dart index 55e988f..b78cdf9 100644 --- a/lib/src/services/reactive_theme/reactive_theme_service.dart +++ b/lib/src/services/reactive_theme/reactive_theme_service.dart @@ -10,7 +10,10 @@ part "reactive_theme_extensions.dart"; /// `ArcaneReactiveTheme` allows switching between light and dark themes and provides /// methods to customize the themes. The current theme mode can be accessed, and the /// 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`. static final ArcaneReactiveTheme _instance = ArcaneReactiveTheme._internal(); @@ -22,6 +25,12 @@ class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver { // Whether to follow system theme 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 _systemThemeNotifier = 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 get currentThemeStream => I._themeStreamController.stream; ThemeMode _currentTheme = ThemeMode.light; + /// The currently active theme mode (light or dark). ThemeMode get currentTheme => _currentTheme; /// 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`. ThemeData get dark => _darkTheme.value; + + /// ValueNotifier for the dark theme that can be observed for changes. ValueNotifier get darkTheme => I._darkTheme; /// Returns the current light theme `ThemeData`. ThemeData get light => _lightTheme.value; + + /// ValueNotifier for the light theme that can be observed for changes. ValueNotifier get lightTheme => I._lightTheme; /// 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 /// 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: /// ```dart /// ArcaneReactiveTheme.I.followSystemTheme(context); - /// final ThemeMode mode = Arcane.theme.systemTheme.value; /// ``` ArcaneReactiveTheme followSystemTheme(BuildContext context) { - if (!_followingSystemTheme) { - _followingSystemTheme = true; + _followingSystemTheme = true; - WidgetsBinding.instance.addPostFrameCallback((_) { - checkSystemTheme(context); - }); - } + // Always check the system theme when this method is called + checkSystemTheme(context); return I; } /// 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) { if (!_followingSystemTheme) return; @@ -109,6 +126,7 @@ class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver { final ThemeMode systemMode = systemBrightness == Brightness.dark ? ThemeMode.dark : ThemeMode.light; + // Only update and notify if the theme actually changed if (systemMode != _currentTheme) { _updateTheme(systemMode); notifyListeners(); @@ -145,6 +163,10 @@ class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver { 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 void reset() { _darkTheme.value = ThemeData.dark(); @@ -154,6 +176,7 @@ class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver { notifyListeners(); } + /// Updates the current theme mode and broadcasts the change. void _updateTheme(ThemeMode themeMode) { _currentTheme = themeMode; _themeStreamController.add(themeMode); diff --git a/lib/src/services/reactive_theme/reactive_theme_wrapper.dart b/lib/src/services/reactive_theme/reactive_theme_wrapper.dart new file mode 100644 index 0000000..d08dce3 --- /dev/null +++ b/lib/src/services/reactive_theme/reactive_theme_wrapper.dart @@ -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(); + } + + @override + bool updateShouldNotify(ArcaneTheme oldWidget) { + return oldWidget.themeMode != themeMode; + } + + static ArcaneReactiveTheme get service => ArcaneReactiveTheme.I; + static bool get isFollowingSystemTheme => service.isFollowingSystemTheme; + static Stream get currentThemeStream => service.currentThemeStream; + static ThemeMode get currentTheme => service.currentTheme; + static ThemeMode get systemTheme => service.systemTheme; + static ThemeData get dark => service.dark; + static ValueNotifier get darkTheme => service.darkTheme; + static ThemeData get light => service.light; + static ValueNotifier 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; +}