Skip to content

Commit

Permalink
basic theme handler
Browse files Browse the repository at this point in the history
  • Loading branch information
aprosail committed Jun 29, 2024
2 parents 8a197ec + ced1424 commit d5fb0a9
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 81 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
26 changes: 22 additions & 4 deletions lib/src/inherit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -87,10 +90,12 @@ class InheritHandler<T> 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;

Expand All @@ -102,7 +107,9 @@ class _InheritHandlerState<T> extends State<InheritHandler<T>> {
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
Expand All @@ -127,7 +134,18 @@ class InheritHandlerAPI<T> {
}

extension WrapInheritHandler on Widget {
Widget handle<T>(T data) => InheritHandler<T>(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>(T data, {void Function(T)? onUpdate}) => InheritHandler<T>(
onUpdate: onUpdate,
data: data,
child: this,
);
}

extension UpdateInheritHandler on BuildContext {
Expand Down
82 changes: 26 additions & 56 deletions lib/src/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Theme> extends StatefulWidget {
const ThemeHandler({
super.key,
Expand All @@ -63,19 +67,9 @@ class _ThemeHandlerState<T extends Theme> extends State<ThemeHandler<T>> {
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
Expand All @@ -84,59 +78,35 @@ class _ThemeHandlerState<T extends Theme> extends State<ThemeHandler<T>> {
@override
void didUpdateWidget(covariant ThemeHandler<T> 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));
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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"}
Expand Down
23 changes: 8 additions & 15 deletions test/inherit_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String>();
Expand Down Expand Up @@ -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;
}
83 changes: 78 additions & 5 deletions test/theme_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
Expand All @@ -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<Brightness>().name}'.asText,
'mode: ${context.findAndTrust<ThemeMode>().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<CustomizedTheme>();
final platformBrightness = MediaQuery.of(context).platformBrightness;
void updateThemeMode(ThemeMode mode) =>
context.updateAndCheck<ThemeMode>((_) => mode);

return [
'platform: ${platformBrightness.name}'.asText,
'brightness: ${context.findAndTrust<Brightness>().name}'.asText,
'mode: ${context.findAndTrust<ThemeMode>().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);
});
}

Expand Down

0 comments on commit d5fb0a9

Please sign in to comment.