diff --git a/example/lib/main.dart b/example/lib/main.dart index 4dde04e..2738f17 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -40,43 +40,45 @@ Future main() async { }, ); - runApp(const MainApp()); -} - -class MainApp extends StatefulWidget { - const MainApp({super.key}); - - @override - State createState() => _MainAppState(); -} - -class _MainAppState extends State { - @override - Widget build(BuildContext context) { - return ArcaneApp( + runApp( + ArcaneApp( services: [ IdService.I, ], - child: MaterialApp( - debugShowCheckedModeBanner: false, - theme: Arcane.theme.light, - darkTheme: Arcane.theme.dark, - themeMode: Arcane.theme.currentTheme, - home: Scaffold( - appBar: AppBar( - title: const Text("Arcane Framework Example"), - actions: [ - IconButton( - icon: const Icon(Icons.contrast), - onPressed: () { - Arcane.theme.switchTheme(); - setState(() {}); - }, - ), - ], - ), - body: const HomeScreen(), + child: const MainApp(), + ), + ); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: Arcane.theme.light, + darkTheme: Arcane.theme.dark, + themeMode: Arcane.theme.currentTheme, + home: Scaffold( + appBar: AppBar( + title: const Text("Arcane Framework Example"), + actions: [ + IconButton( + icon: const Icon(Icons.settings_system_daydream), + onPressed: () { + Arcane.theme.followSystemTheme(context); + }, + ), + IconButton( + icon: const Icon(Icons.contrast), + onPressed: () { + Arcane.theme.switchTheme(); + }, + ), + ], ), + body: const HomeScreen(), ), ); } @@ -116,11 +118,11 @@ class _HomeScreenState extends State { ElevatedButton( child: const Text("Sign in"), onPressed: () async { - await Arcane.auth.login>( - input: { - "email": "email", - "password": "password", - }, + await Arcane.auth.login( + input: ( + email: "email", + password: "password", + ), onLoggedIn: () async { setState(() {}); }, diff --git a/lib/src/arcane_app.dart b/lib/src/arcane_app.dart index 01ddd0d..b62ef5f 100644 --- a/lib/src/arcane_app.dart +++ b/lib/src/arcane_app.dart @@ -18,7 +18,7 @@ import "package:flutter/material.dart"; /// child: MyApp(), /// ); /// ``` -class ArcaneApp extends StatelessWidget { +class ArcaneApp extends StatefulWidget { /// A list of Arcane services that will be made available to the application. /// /// These services will be provided to the widget tree using @@ -42,13 +42,70 @@ class ArcaneApp extends StatelessWidget { super.key, }); + @override + State createState() => _ArcaneAppState(); +} + +class _ArcaneAppState extends State with WidgetsBindingObserver { + final GlobalKey _appKey = GlobalKey(); + @override Widget build(BuildContext context) { return ArcaneEnvironmentProvider( child: ArcaneServiceProvider( - serviceInstances: services, - child: child, + serviceInstances: widget.services, + child: Builder( + key: _appKey, + builder: (context) { + _updateContextReference(context); + + return StreamBuilder( + stream: ArcaneReactiveTheme.I.currentThemeStream, + builder: (context, AsyncSnapshot snapshot) { + if (!snapshot.hasData) return widget.child; + + return KeyedSubtree( + key: Key(snapshot.data!.name), + child: widget.child, + ); + }, + ); + }, + ), ), ); } + + // 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(); + // Register as an observer to detect system theme changes + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + // Clean up the observer when the widget is disposed + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @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); + super.didChangePlatformBrightness(); + } } diff --git a/lib/src/services/reactive_theme/reactive_theme_extensions.dart b/lib/src/services/reactive_theme/reactive_theme_extensions.dart index 604bd18..edb18f6 100644 --- a/lib/src/services/reactive_theme/reactive_theme_extensions.dart +++ b/lib/src/services/reactive_theme/reactive_theme_extensions.dart @@ -15,7 +15,7 @@ extension DarkMode on BuildContext { /// } /// ``` bool get isDarkMode { - final brightness = MediaQuery.of(this).platformBrightness; + final brightness = MediaQuery.platformBrightnessOf(this); return brightness == Brightness.dark; } } diff --git a/lib/src/services/reactive_theme/reactive_theme_service.dart b/lib/src/services/reactive_theme/reactive_theme_service.dart index 1ceca61..55e988f 100644 --- a/lib/src/services/reactive_theme/reactive_theme_service.dart +++ b/lib/src/services/reactive_theme/reactive_theme_service.dart @@ -1,3 +1,5 @@ +import "dart:async"; + import "package:arcane_framework/arcane_framework.dart"; import "package:flutter/material.dart"; @@ -8,7 +10,7 @@ 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 { +class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver { /// The singleton instance of `ArcaneReactiveTheme`. static final ArcaneReactiveTheme _instance = ArcaneReactiveTheme._internal(); @@ -17,13 +19,24 @@ class ArcaneReactiveTheme extends ArcaneService { ArcaneReactiveTheme._internal(); + // Whether to follow system theme + bool _followingSystemTheme = false; + final ValueNotifier _systemThemeNotifier = - ValueNotifier(ThemeMode.light); + ValueNotifier(ThemeMode.system); - final ValueNotifier _currentThemeNotifier = - ValueNotifier(ThemeMode.light); + final StreamController _themeStreamController = + StreamController.broadcast( + onCancel: () { + I._themeStreamController.close(); + }, + ); - ThemeMode get currentTheme => I._currentThemeNotifier.value; + Stream get currentThemeStream => I._themeStreamController.stream; + + ThemeMode _currentTheme = ThemeMode.light; + + ThemeMode get currentTheme => _currentTheme; /// A listenable that notifies listeners when the system theme mode changes. ThemeMode get systemTheme => I._systemThemeNotifier.value; @@ -52,21 +65,17 @@ class ArcaneReactiveTheme extends ArcaneService { /// ArcaneReactiveTheme.I.switchTheme(); /// ``` ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) { - if (I._systemThemeNotifier.hasListeners) { - _systemThemeNotifier.removeListener(_systemThemeListener); - } + _followingSystemTheme = false; if (themeMode != null) { - _currentThemeNotifier.value = themeMode; + _updateTheme(themeMode); } else { - _currentThemeNotifier.value = - _currentThemeNotifier.value == ThemeMode.light - ? ThemeMode.dark - : ThemeMode.light; + _updateTheme( + currentTheme == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark, + ); } notifyListeners(); - return I; } @@ -79,21 +88,33 @@ class ArcaneReactiveTheme extends ArcaneService { /// final ThemeMode mode = Arcane.theme.systemTheme.value; /// ``` ArcaneReactiveTheme followSystemTheme(BuildContext context) { - final ThemeMode systemMode = - context.isDarkMode ? ThemeMode.dark : ThemeMode.light; + if (!_followingSystemTheme) { + _followingSystemTheme = true; - if (!I._systemThemeNotifier.hasListeners) { - I._systemThemeNotifier.addListener(_systemThemeListener); - } - - if (systemMode != currentTheme) { - _systemThemeNotifier.value = systemMode; - notifyListeners(); + WidgetsBinding.instance.addPostFrameCallback((_) { + checkSystemTheme(context); + }); } return I; } + /// Check and apply the system theme if we're following it + void checkSystemTheme(BuildContext context) { + if (!_followingSystemTheme) return; + + final Brightness systemBrightness = + MediaQuery.platformBrightnessOf(context); + + final ThemeMode systemMode = + systemBrightness == Brightness.dark ? ThemeMode.dark : ThemeMode.light; + + if (systemMode != _currentTheme) { + _updateTheme(systemMode); + notifyListeners(); + } + } + /// Sets a custom `ThemeData` for the dark theme. /// /// This allows you to customize the dark theme and notify listeners to apply the @@ -128,15 +149,13 @@ class ArcaneReactiveTheme extends ArcaneService { void reset() { _darkTheme.value = ThemeData.dark(); _lightTheme.value = ThemeData.light(); - _systemThemeNotifier.value = ThemeMode.light; - _currentThemeNotifier.value = ThemeMode.light; + _followingSystemTheme = false; + _updateTheme(ThemeMode.light); notifyListeners(); } - void _systemThemeListener() { - if (currentTheme != _systemThemeNotifier.value) { - _currentThemeNotifier.value = _systemThemeNotifier.value; - notifyListeners(); - } + void _updateTheme(ThemeMode themeMode) { + _currentTheme = themeMode; + _themeStreamController.add(themeMode); } }