From 707a4c7956fc6c7173ec1eda1a829ec40c133571 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Tue, 1 Jul 2025 17:05:05 +0200 Subject: [PATCH] Refactor theme management to use ListenableBuilder for dynamic theme updates Signed-off-by: Hans Kokx --- example/lib/main.dart | 310 +++++++++--------- .../reactive_theme_service.dart | 10 + 2 files changed, 166 insertions(+), 154 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index a54d325..a92dde5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -269,170 +269,172 @@ class ArcaneThemeExample extends StatelessWidget { const ArcaneThemeExample({ super.key, }); - @override Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Theme", - style: Theme.of(context).textTheme.headlineSmall, - ), - Column( + return ListenableBuilder( + listenable: Arcane.theme.themeChanges, + builder: (context, _) { + final ThemeData effectiveTheme = Arcane.theme.currentTheme; + final bool isFollowingSystem = Arcane.theme.isFollowingSystemTheme; + + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Switch( - value: Arcane.theme.currentThemeMode == ThemeMode.dark, - thumbIcon: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.selected)) { - return const Icon(Icons.dark_mode); - } - return const Icon(Icons.light_mode); - }), - onChanged: (_) { - final ThemeMode oldTheme = Arcane.theme.currentThemeMode; - Arcane.theme.switchTheme(); - Arcane.log( - "Switching theme", - metadata: { - "followingSystemTheme": - "${Arcane.theme.isFollowingSystemTheme}", - "newMode": Arcane.theme.currentThemeMode.name, - "oldMode": oldTheme.name, - }, - ); - }, + Text( + "Theme", + style: Theme.of(context).textTheme.headlineSmall, ), - Row( - mainAxisSize: MainAxisSize.min, + Column( children: [ - ValueListenableBuilder( - valueListenable: Arcane.theme.followingSystemThemeChanges, - builder: (context, isFollowingSystem, _) => Checkbox( - value: isFollowingSystem, - onChanged: (value) { - final ThemeMode oldTheme = - Arcane.theme.currentThemeMode; - if (value == true) { - Arcane.theme.followSystemTheme(context); - Arcane.log( - "Switching theme", - metadata: { - "followingSystemTheme": - "${Arcane.theme.isFollowingSystemTheme}", - "newMode": Arcane.theme.currentThemeMode.name, - "oldMode": oldTheme.name, - }, - ); - } else { - Arcane.theme.switchTheme( - themeMode: Arcane.theme.systemThemeMode, - ); - Arcane.log( - "Switching theme", - metadata: { - "followingSystemTheme": - "${Arcane.theme.isFollowingSystemTheme}", - "newMode": Arcane.theme.currentThemeMode.name, - "oldMode": oldTheme.name, - }, - ); - } - }, - ), + Switch( + value: Arcane.theme.currentThemeMode == ThemeMode.dark, + thumbIcon: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return const Icon(Icons.dark_mode); + } + return const Icon(Icons.light_mode); + }), + onChanged: (_) { + final ThemeMode oldTheme = + Arcane.theme.currentThemeMode; + Arcane.theme.switchTheme(); + Arcane.log( + "Switching theme", + metadata: { + "followingSystemTheme": + "${Arcane.theme.isFollowingSystemTheme}", + "newMode": Arcane.theme.currentThemeMode.name, + "oldMode": oldTheme.name, + }, + ); + }, + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox( + value: isFollowingSystem, + onChanged: (value) { + final ThemeMode oldTheme = + Arcane.theme.currentThemeMode; + if (value == true) { + Arcane.theme.followSystemTheme(context); + Arcane.log( + "Switching theme", + metadata: { + "followingSystemTheme": + "${Arcane.theme.isFollowingSystemTheme}", + "newMode": Arcane.theme.currentThemeMode.name, + "oldMode": oldTheme.name, + }, + ); + } else { + Arcane.theme.switchTheme( + themeMode: Arcane.theme.systemThemeMode, + ); + Arcane.log( + "Switching theme", + metadata: { + "followingSystemTheme": + "${Arcane.theme.isFollowingSystemTheme}", + "newMode": Arcane.theme.currentThemeMode.name, + "oldMode": oldTheme.name, + }, + ); + } + }, + ), + const Text("Follow system"), + ], ), - const Text("Follow system"), ], ), + SizedBox( + height: 20, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 8, + children: [ + const Text("Color"), + Expanded( + child: ListView.separated( + itemCount: colors.length, + scrollDirection: Axis.horizontal, + separatorBuilder: (_, __) => const SizedBox(width: 4), + itemBuilder: (context, index) { + return InkWell( + onTap: () { + if (context.themeMode == ThemeMode.dark) { + Arcane.theme.setDarkTheme( + ThemeData( + brightness: Brightness.dark, + colorSchemeSeed: colors[index], + ), + ); + } else if (context.themeMode == + ThemeMode.light) { + Arcane.theme.setLightTheme( + ThemeData( + brightness: Brightness.light, + colorSchemeSeed: colors[index], + ), + ); + } + + Arcane.log( + "Setting ${Arcane.theme.currentThemeMode.name} theme color to ${colors[index].name}", + ); + }, + child: Container( + key: Key( + "${colors[index]}-${Arcane.theme.currentThemeMode}"), + decoration: BoxDecoration( + color: colors[index], + border: + effectiveTheme.colorScheme.primary.name == + colors[index].name + ? Border.all( + width: 2, + color: Colors.white, + ) + : null, + boxShadow: + effectiveTheme.colorScheme.primary.name == + colors[index].name + ? [ + const BoxShadow( + color: Colors.black, + spreadRadius: 1, + blurRadius: 1, + offset: Offset(0, 0), + ), + ] + : null, + ), + width: 20, + height: 20, + ), + ); + }, + ), + ), + ], + ), + ), + Text( + "The current theme mode is ${Arcane.theme.currentModeOf(context).name} and " + "is ${Arcane.theme.isFollowingSystemTheme ? "" : "not "}" + "following the system theme.", + ), ], ), - SizedBox( - height: 20, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - spacing: 8, - children: [ - const Text("Color"), - Expanded( - child: ValueListenableBuilder( - valueListenable: Arcane.theme.effectiveThemeChanges, - builder: (context, effectiveTheme, _) => - ListView.separated( - itemCount: colors.length, - scrollDirection: Axis.horizontal, - separatorBuilder: (_, __) => const SizedBox(width: 4), - itemBuilder: (context, index) { - return InkWell( - onTap: () { - if (context.themeMode == ThemeMode.dark) { - Arcane.theme.setDarkTheme( - ThemeData( - brightness: Brightness.dark, - colorSchemeSeed: colors[index], - ), - ); - } else if (context.themeMode == ThemeMode.light) { - Arcane.theme.setLightTheme( - ThemeData( - brightness: Brightness.light, - colorSchemeSeed: colors[index], - ), - ); - } - - Arcane.log( - "Setting ${Arcane.theme.currentThemeMode.name} theme color to ${colors[index].name}", - ); - }, - child: Container( - key: Key( - "${colors[index]}-${Arcane.theme.currentThemeMode}"), - decoration: BoxDecoration( - color: colors[index], - border: - effectiveTheme.colorScheme.primary.name == - colors[index].name - ? Border.all( - width: 2, - color: Colors.white, - ) - : null, - boxShadow: - effectiveTheme.colorScheme.primary.name == - colors[index].name - ? [ - const BoxShadow( - color: Colors.black, - spreadRadius: 1, - blurRadius: 1, - offset: Offset(0, 0), - ), - ] - : null, - ), - width: 20, - height: 20, - ), - ); - }, - ), - ), - ), - ], - ), - ), - Text( - "The current theme mode is ${Arcane.theme.currentModeOf(context).name} and " - "is ${Arcane.theme.isFollowingSystemTheme ? "" : "not "}" - "following the system theme.", - ), - ], - ), - ), + ), + ); + }, ); } } diff --git a/lib/src/services/reactive_theme/reactive_theme_service.dart b/lib/src/services/reactive_theme/reactive_theme_service.dart index a0f9bf0..3c75e36 100644 --- a/lib/src/services/reactive_theme/reactive_theme_service.dart +++ b/lib/src/services/reactive_theme/reactive_theme_service.dart @@ -82,6 +82,16 @@ class ArcaneReactiveTheme extends ArcaneService { final ValueNotifier _followingSystemThemeNotifier = ValueNotifier(false); + /// Combined Listenable that merges all theme-related changes. + /// Use this for widgets that need to rebuild on any theme change. + Listenable get themeChanges => I._combinedThemeListenable; + + late final Listenable _combinedThemeListenable = Listenable.merge([ + _effectiveThemeNotifier, + _followingSystemThemeNotifier, + _themeModeNotifier, + ]); + // ************************************************************************ // // * MARK: Light/Dark theme // ************************************************************************ //