Fixes the reactive theme service to properly follow the system brightness

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2025-04-23 20:56:49 +02:00
parent 58817b349d
commit cfd9052442
4 changed files with 150 additions and 72 deletions
+40 -38
View File
@@ -40,43 +40,45 @@ Future<void> main() async {
}, },
); );
runApp(const MainApp()); runApp(
} ArcaneApp(
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
@override
Widget build(BuildContext context) {
return ArcaneApp(
services: [ services: [
IdService.I, IdService.I,
], ],
child: MaterialApp( child: const MainApp(),
debugShowCheckedModeBanner: false, ),
theme: Arcane.theme.light, );
darkTheme: Arcane.theme.dark, }
themeMode: Arcane.theme.currentTheme,
home: Scaffold( class MainApp extends StatelessWidget {
appBar: AppBar( const MainApp({super.key});
title: const Text("Arcane Framework Example"),
actions: [ @override
IconButton( Widget build(BuildContext context) {
icon: const Icon(Icons.contrast), return MaterialApp(
onPressed: () { debugShowCheckedModeBanner: false,
Arcane.theme.switchTheme(); theme: Arcane.theme.light,
setState(() {}); darkTheme: Arcane.theme.dark,
}, themeMode: Arcane.theme.currentTheme,
), home: Scaffold(
], appBar: AppBar(
), title: const Text("Arcane Framework Example"),
body: const HomeScreen(), 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<HomeScreen> {
ElevatedButton( ElevatedButton(
child: const Text("Sign in"), child: const Text("Sign in"),
onPressed: () async { onPressed: () async {
await Arcane.auth.login<Map<String, String>>( await Arcane.auth.login<Credentials>(
input: { input: (
"email": "email", email: "email",
"password": "password", password: "password",
}, ),
onLoggedIn: () async { onLoggedIn: () async {
setState(() {}); setState(() {});
}, },
+60 -3
View File
@@ -18,7 +18,7 @@ import "package:flutter/material.dart";
/// child: MyApp(), /// child: MyApp(),
/// ); /// );
/// ``` /// ```
class ArcaneApp extends StatelessWidget { class ArcaneApp extends StatefulWidget {
/// A list of Arcane services that will be made available to the application. /// A list of Arcane services that will be made available to the application.
/// ///
/// These services will be provided to the widget tree using /// These services will be provided to the widget tree using
@@ -42,13 +42,70 @@ class ArcaneApp extends StatelessWidget {
super.key, super.key,
}); });
@override
State<ArcaneApp> createState() => _ArcaneAppState();
}
class _ArcaneAppState extends State<ArcaneApp> with WidgetsBindingObserver {
final GlobalKey _appKey = GlobalKey();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ArcaneEnvironmentProvider( return ArcaneEnvironmentProvider(
child: ArcaneServiceProvider( child: ArcaneServiceProvider(
serviceInstances: services, serviceInstances: widget.services,
child: child, child: Builder(
key: _appKey,
builder: (context) {
_updateContextReference(context);
return StreamBuilder<ThemeMode>(
stream: ArcaneReactiveTheme.I.currentThemeStream,
builder: (context, AsyncSnapshot<ThemeMode> 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();
}
} }
@@ -15,7 +15,7 @@ extension DarkMode on BuildContext {
/// } /// }
/// ``` /// ```
bool get isDarkMode { bool get isDarkMode {
final brightness = MediaQuery.of(this).platformBrightness; final brightness = MediaQuery.platformBrightnessOf(this);
return brightness == Brightness.dark; return brightness == Brightness.dark;
} }
} }
@@ -1,3 +1,5 @@
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";
@@ -8,7 +10,7 @@ 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 { class ArcaneReactiveTheme extends ArcaneService with WidgetsBindingObserver {
/// The singleton instance of `ArcaneReactiveTheme`. /// The singleton instance of `ArcaneReactiveTheme`.
static final ArcaneReactiveTheme _instance = ArcaneReactiveTheme._internal(); static final ArcaneReactiveTheme _instance = ArcaneReactiveTheme._internal();
@@ -17,13 +19,24 @@ class ArcaneReactiveTheme extends ArcaneService {
ArcaneReactiveTheme._internal(); ArcaneReactiveTheme._internal();
// Whether to follow system theme
bool _followingSystemTheme = false;
final ValueNotifier<ThemeMode> _systemThemeNotifier = final ValueNotifier<ThemeMode> _systemThemeNotifier =
ValueNotifier(ThemeMode.light); ValueNotifier(ThemeMode.system);
final ValueNotifier<ThemeMode> _currentThemeNotifier = final StreamController<ThemeMode> _themeStreamController =
ValueNotifier(ThemeMode.light); StreamController<ThemeMode>.broadcast(
onCancel: () {
I._themeStreamController.close();
},
);
ThemeMode get currentTheme => I._currentThemeNotifier.value; Stream<ThemeMode> get currentThemeStream => I._themeStreamController.stream;
ThemeMode _currentTheme = ThemeMode.light;
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.
ThemeMode get systemTheme => I._systemThemeNotifier.value; ThemeMode get systemTheme => I._systemThemeNotifier.value;
@@ -52,21 +65,17 @@ class ArcaneReactiveTheme extends ArcaneService {
/// ArcaneReactiveTheme.I.switchTheme(); /// ArcaneReactiveTheme.I.switchTheme();
/// ``` /// ```
ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) { ArcaneReactiveTheme switchTheme({ThemeMode? themeMode}) {
if (I._systemThemeNotifier.hasListeners) { _followingSystemTheme = false;
_systemThemeNotifier.removeListener(_systemThemeListener);
}
if (themeMode != null) { if (themeMode != null) {
_currentThemeNotifier.value = themeMode; _updateTheme(themeMode);
} else { } else {
_currentThemeNotifier.value = _updateTheme(
_currentThemeNotifier.value == ThemeMode.light currentTheme == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark,
? ThemeMode.dark );
: ThemeMode.light;
} }
notifyListeners(); notifyListeners();
return I; return I;
} }
@@ -79,21 +88,33 @@ class ArcaneReactiveTheme extends ArcaneService {
/// final ThemeMode mode = Arcane.theme.systemTheme.value; /// final ThemeMode mode = Arcane.theme.systemTheme.value;
/// ``` /// ```
ArcaneReactiveTheme followSystemTheme(BuildContext context) { ArcaneReactiveTheme followSystemTheme(BuildContext context) {
final ThemeMode systemMode = if (!_followingSystemTheme) {
context.isDarkMode ? ThemeMode.dark : ThemeMode.light; _followingSystemTheme = true;
if (!I._systemThemeNotifier.hasListeners) { WidgetsBinding.instance.addPostFrameCallback((_) {
I._systemThemeNotifier.addListener(_systemThemeListener); checkSystemTheme(context);
} });
if (systemMode != currentTheme) {
_systemThemeNotifier.value = systemMode;
notifyListeners();
} }
return I; 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. /// Sets a custom `ThemeData` for the dark theme.
/// ///
/// This allows you to customize the dark theme and notify listeners to apply the /// This allows you to customize the dark theme and notify listeners to apply the
@@ -128,15 +149,13 @@ class ArcaneReactiveTheme extends ArcaneService {
void reset() { void reset() {
_darkTheme.value = ThemeData.dark(); _darkTheme.value = ThemeData.dark();
_lightTheme.value = ThemeData.light(); _lightTheme.value = ThemeData.light();
_systemThemeNotifier.value = ThemeMode.light; _followingSystemTheme = false;
_currentThemeNotifier.value = ThemeMode.light; _updateTheme(ThemeMode.light);
notifyListeners(); notifyListeners();
} }
void _systemThemeListener() { void _updateTheme(ThemeMode themeMode) {
if (currentTheme != _systemThemeNotifier.value) { _currentTheme = themeMode;
_currentThemeNotifier.value = _systemThemeNotifier.value; _themeStreamController.add(themeMode);
notifyListeners();
}
} }
} }