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
+24 -22
View File
@@ -40,24 +40,22 @@ Future<void> main() async {
},
);
runApp(const MainApp());
}
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(
runApp(
ArcaneApp(
services: [
IdService.I,
],
child: MaterialApp(
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,
@@ -66,18 +64,22 @@ class _MainAppState extends State<MainApp> {
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();
setState(() {});
},
),
],
),
body: const HomeScreen(),
),
),
);
}
}
@@ -116,11 +118,11 @@ class _HomeScreenState extends State<HomeScreen> {
ElevatedButton(
child: const Text("Sign in"),
onPressed: () async {
await Arcane.auth.login<Map<String, String>>(
input: {
"email": "email",
"password": "password",
},
await Arcane.auth.login<Credentials>(
input: (
email: "email",
password: "password",
),
onLoggedIn: () async {
setState(() {});
},
+60 -3
View File
@@ -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<ArcaneApp> createState() => _ArcaneAppState();
}
class _ArcaneAppState extends State<ArcaneApp> 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<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 {
final brightness = MediaQuery.of(this).platformBrightness;
final brightness = MediaQuery.platformBrightnessOf(this);
return brightness == Brightness.dark;
}
}
@@ -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<ThemeMode> _systemThemeNotifier =
ValueNotifier(ThemeMode.light);
ValueNotifier(ThemeMode.system);
final ValueNotifier<ThemeMode> _currentThemeNotifier =
ValueNotifier(ThemeMode.light);
final StreamController<ThemeMode> _themeStreamController =
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.
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);
}
}