diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b18e4e..928ebf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.4.0 + +- Theme template and theme handler. +- Support customized theme on mixin. +- Theme handler adapt system brightness change. +- Remove unnecessary encapsulations and tidy comments. +- Inherit on change trigger (experimental). + ## 0.3.0 - Wrap media with default value. diff --git a/lib/src/inherit.dart b/lib/src/inherit.dart index 19ee4a6..36964a7 100644 --- a/lib/src/inherit.dart +++ b/lib/src/inherit.dart @@ -75,8 +75,11 @@ extension FindInherit on BuildContext { } /// A stateful widget to handle [Inherit]ed data in widget tree. -/// You can change the handled data from the descendants in the widget tree -/// using the [BuildContext]'s [InheritHandlerAPI.update] extension method. +/// +/// 1. You can change the handled data from the descendants in the widget tree +/// using the [BuildContext]'s [InheritHandlerAPI.update] extension method. +/// 2. You can also customize [onUpdate] callback to resolve +/// actions when the value changed. /// /// It's strongly not recommended to use it directly, /// please consider using [WrapInheritHandler.handle] extended on [Widget] @@ -87,10 +90,12 @@ class InheritHandler extends StatefulWidget { /// before using such constructor directly. const InheritHandler({ super.key, + this.onUpdate, required this.data, required this.child, }); + final void Function(T value)? onUpdate; final T data; final Widget child; @@ -102,7 +107,9 @@ class _InheritHandlerState extends State> { late T _data = widget.data; void update(T value) { - if (_data != value) setState(() => _data = value); + if (_data == value) return; + setState(() => _data = value); + widget.onUpdate?.call(value); } @override @@ -127,7 +134,18 @@ class InheritHandlerAPI { } extension WrapInheritHandler on Widget { - Widget handle(T data) => InheritHandler(data: data, child: this); + /// Handle a data of type [T] into the widget tree. + /// + /// 1. This extension method is an encapsulation of [InheritHandler]. + /// 2. You can use [UpdateInheritHandler.update] extension method + /// to modify the handled value. + /// 3. You can also specify [onUpdate] to register trigger actions + /// when the value changed. + Widget handle(T data, {void Function(T)? onUpdate}) => InheritHandler( + onUpdate: onUpdate, + data: data, + child: this, + ); } extension UpdateInheritHandler on BuildContext { diff --git a/lib/src/theme.dart b/lib/src/theme.dart index 5d8eb14..1c61fb6 100644 --- a/lib/src/theme.dart +++ b/lib/src/theme.dart @@ -40,6 +40,10 @@ extension WrapTheme on Widget { ); } +/// Handle a [Theme] and the [ThemeMode], +/// and it will also provide the [Brightness] of current theme. +/// And you can also modify current [Theme] and [ThemeMode] from +/// its descendants in the widget tree via the context. class ThemeHandler extends StatefulWidget { const ThemeHandler({ super.key, @@ -63,19 +67,9 @@ class _ThemeHandlerState extends State> { late T _dark = widget.dark; late ThemeMode _mode = widget.mode; - late T _theme = theme; - late Brightness _brightness = brightness; - - /// Compute what theme ([T]) should be now according to [_brightness]. - /// It's not recommended to call it directly, consider using [_theme] - /// to reduce unnecessary computations. - T get theme => _brightness == Brightness.dark ? _dark : _light; - /// Compute what [Brightness] should be now according to [_mode] /// and [MediaQueryData.platformBrightness] of current platform. - /// It's not recommended to call it directly, consider using [_brightness] - /// to reduce unnecessary computations. - Brightness get brightness => _mode == ThemeMode.system + Brightness get adaptedBrightness => _mode == ThemeMode.system ? MediaQuery.of(context).platformBrightness : _mode == ThemeMode.dark ? Brightness.dark @@ -84,59 +78,35 @@ class _ThemeHandlerState extends State> { @override void didUpdateWidget(covariant ThemeHandler oldWidget) { super.didUpdateWidget(oldWidget); - - var needSetState = false; - if (widget.mode != _mode) needSetState = _preUpdateMode(widget.mode); - - if (widget.light != _light) { - _light = widget.light; - if (_brightness == Brightness.light) needSetState = true; - } - if (widget.dark != _dark) { - _dark = widget.dark; - if (_brightness == Brightness.dark) needSetState = true; - } - - if (needSetState) setState(() {}); + if (_mode != widget.mode) setState(() => _mode = widget.mode); + if (_dark != widget.dark) setState(() => _dark = widget.dark); + if (_light != widget.light) setState(() => _light = widget.light); } - void updateMode(ThemeMode Function(ThemeMode raw) updater) { - if (_preUpdateMode(updater(_mode))) setState(() {}); + void updateMode(ThemeMode mode) { + if (_mode != mode) setState(() => _mode = mode); } - bool _preUpdateMode(ThemeMode mode) { - if (mode == _mode) return false; - _mode = mode; - final brightness = this.brightness; - if (_brightness != brightness) { - _brightness = brightness; - _theme = theme; - } - return true; - } - - void updateCurrentTheme(T Function(T raw) updater) { - if (_preUpdateCurrentTheme(updater(_theme))) setState(() {}); - } - - bool _preUpdateCurrentTheme(T theme) { - if (_theme == theme) return false; - switch (_brightness) { + void updateCurrentTheme(T theme) { + switch (adaptedBrightness) { case Brightness.dark: - _dark = theme; + if (_dark != theme) setState(() => _dark = theme); case Brightness.light: - _light = theme; + if (_light != theme) setState(() => _light = theme); } - return true; } @override - Widget build(BuildContext context) => widget.child - .foreground(context, _theme.foreground) - .background(_theme.background) - .inherit(_brightness) - .inherit(_theme) - .inherit(_mode) - .inherit(InheritHandlerAPI(updateMode)) - .inherit(InheritHandlerAPI(updateCurrentTheme)); + Widget build(BuildContext context) { + final brightness = adaptedBrightness; + final theme = adaptedBrightness == Brightness.dark ? _dark : _light; + return widget.child + .foreground(context, theme.foreground) + .background(theme.background) + .inherit(brightness) + .inherit(theme) + .inherit(_mode) + .inherit(InheritHandlerAPI(updateMode)) + .inherit(InheritHandlerAPI(updateCurrentTheme)); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 0662e46..26e1a25 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: modifier description: Syntax sugar optimizations to avoid nesting hell in Flutter. -version: 0.3.0 +version: 0.4.0 homepage: https://github.com/treeinfra/modifier repository: https://github.com/treeinfra/modifier environment: {sdk: ">=3.4.3 <4.0.0", flutter: ">=3.22.2"} diff --git a/test/inherit_test.dart b/test/inherit_test.dart index 1d0955a..5b61e1e 100644 --- a/test/inherit_test.dart +++ b/test/inherit_test.dart @@ -4,19 +4,6 @@ import 'package:modifier/modifier.dart'; import 'package:modifier_test/modifier_test.dart'; void main() { - testFindAndTrust(); - testInheritHandler(); -} - -/// Wrapping a single text message for demonstration. -/// See the code inside [testFindAndTrust]. -class MessageExample { - const MessageExample({required this.message}); - - final String message; -} - -void testFindAndTrust() { group('find and trust', () { // It's strongly not recommended to code like that, // because there might many inherited data with the String type @@ -44,9 +31,7 @@ void testFindAndTrust() { expect(find.text(message), findsOneWidget); }); }); -} -void testInheritHandler() { testWidgets('inherit handler', (t) async { await builder((context) { final message = context.findAndTrust(); @@ -109,3 +94,11 @@ void testInheritHandler() { expect(find.text('outer message: 123457'), findsOneWidget); }); } + +/// Wrapping a single text message for demonstration. +/// This is an encapsulation to avoid inherit the commonly used [String] type. +class MessageExample { + const MessageExample({required this.message}); + + final String message; +} diff --git a/test/theme_test.dart b/test/theme_test.dart index c1345a3..fd4f00b 100644 --- a/test/theme_test.dart +++ b/test/theme_test.dart @@ -6,6 +6,9 @@ import 'package:modifier/modifier.dart'; import 'package:modifier_test/modifier_test.dart'; void main() { + const light = CustomizedTheme.light(); + const dark = CustomizedTheme.dark(); + testWidgets('platform media', (t) async { await builder((context) => 'brightness: ${MediaQuery.of(context).platformBrightness.name}' @@ -27,11 +30,81 @@ void main() { expect(find.text('brightness: ${Brightness.dark.name}'), findsOneWidget); }); - testWidgets('theme adapt', (t) async { - const light = CustomizedTheme.light(); - expect(light, light); - const dark = CustomizedTheme.dark(); - expect(dark, dark); + testWidgets('brightness adapt', (t) async { + await builder((context) { + final platformBrightness = MediaQuery.of(context).platformBrightness; + return [ + 'platform: ${platformBrightness.name}'.asText, + 'brightness: ${context.findAndTrust().name}'.asText, + 'mode: ${context.findAndTrust().name}'.asText, + ].asColumn; + }) + .center + .theme(light: light, dark: dark) + .builder((context, child) => child.ensureDirection(context)) + .builder((context, child) => child.ensureMedia(context)) + .pump(t); + + t.binding.platformDispatcher.platformBrightnessTestValue = Brightness.light; + await t.pump(); + expect(find.text('platform: ${Brightness.light.name}'), findsOneWidget); + expect(find.text('brightness: ${Brightness.light.name}'), findsOneWidget); + expect(find.text('mode: ${ThemeMode.system.name}'), findsOneWidget); + + t.binding.platformDispatcher.platformBrightnessTestValue = Brightness.dark; + await t.pump(); + expect(find.text('platform: ${Brightness.dark.name}'), findsOneWidget); + expect(find.text('brightness: ${Brightness.dark.name}'), findsOneWidget); + expect(find.text('mode: ${ThemeMode.system.name}'), findsOneWidget); + }); + + testWidgets('change theme mode', (t) async { + await builder((context) { + final theme = context.findAndTrust(); + final platformBrightness = MediaQuery.of(context).platformBrightness; + void updateThemeMode(ThemeMode mode) => + context.updateAndCheck((_) => mode); + + return [ + 'platform: ${platformBrightness.name}'.asText, + 'brightness: ${context.findAndTrust().name}'.asText, + 'mode: ${context.findAndTrust().name}'.asText, + 'background: ${theme.background.hex}'.asText, + 'foreground: ${theme.foreground.hex}'.asText, + 'to system'.asText.on(tap: () => updateThemeMode(ThemeMode.system)), + 'to light'.asText.on(tap: () => updateThemeMode(ThemeMode.light)), + 'to dark'.asText.on(tap: () => updateThemeMode(ThemeMode.dark)), + ].asColumn; + }) + .center + .theme(light: light, dark: dark) + .builder((context, child) => child.ensureDirection(context)) + .builder((context, child) => child.ensureMedia(context)) + .pump(t); + + t.binding.platformDispatcher.platformBrightnessTestValue = Brightness.light; + await t.pump(); + expect(find.text('platform: ${Brightness.light.name}'), findsOneWidget); + expect(find.text('brightness: ${Brightness.light.name}'), findsOneWidget); + expect(find.text('mode: ${ThemeMode.system.name}'), findsOneWidget); + expect(find.text('background: ${light.background.hex}'), findsOneWidget); + expect(find.text('foreground: ${light.foreground.hex}'), findsOneWidget); + + await t.tap(find.text('to dark')); + await t.pump(); + expect(find.text('platform: ${Brightness.light.name}'), findsOneWidget); + expect(find.text('brightness: ${Brightness.dark.name}'), findsOneWidget); + expect(find.text('mode: ${ThemeMode.dark.name}'), findsOneWidget); + expect(find.text('background: ${dark.background.hex}'), findsOneWidget); + expect(find.text('foreground: ${dark.foreground.hex}'), findsOneWidget); + + await t.tap(find.text('to light')); + await t.pump(); + expect(find.text('platform: ${Brightness.light.name}'), findsOneWidget); + expect(find.text('brightness: ${Brightness.light.name}'), findsOneWidget); + expect(find.text('mode: ${ThemeMode.light.name}'), findsOneWidget); + expect(find.text('background: ${light.background.hex}'), findsOneWidget); + expect(find.text('foreground: ${light.foreground.hex}'), findsOneWidget); }); }