From 2d23c8ca5427d06cd50efcefd61760f2b08f90e2 Mon Sep 17 00:00:00 2001 From: Rody Davis Date: Wed, 27 Nov 2024 17:24:01 -0800 Subject: [PATCH] Add WatchBuilder to ease migration --- benchmark/pubspec.lock | 56 ++++---- examples/animations_example/lib/main.dart | 2 +- examples/auth_flow/lib/main.dart | 6 +- .../lib/presentation/views/edit_todo.dart | 2 +- .../lib/presentation/views/todo_list.dart | 2 +- examples/crud_dio/lib/ui/home.dart | 2 +- examples/crud_dio/lib/ui/home_detail.dart | 2 +- .../lib/screens/home/drawer.dart | 2 +- .../lib/screens/spreadsheet.dart | 4 +- .../eval_calculator/lib/screens/table.dart | 4 +- examples/flutter_async/lib/main.dart | 6 +- .../flutter_colorband/lib/color_scale.dart | 2 +- .../lib/color_scale_band.dart | 2 +- .../lib/color_scale_selector.dart | 6 +- examples/get_it_signals/lib/main.dart | 6 +- examples/infinite_scroll/lib/main.dart | 2 +- examples/node_based_editor/lib/editor.dart | 8 +- .../node_based_editor/lib/nodes/datetime.dart | 2 +- .../node_based_editor/lib/nodes/time.dart | 2 +- packages/signals/pubspec.yaml | 6 +- packages/signals_core/pubspec.yaml | 4 +- .../lib/src/node_table.dart | 2 +- .../lib/examples/computed_reorder.dart | 2 +- .../example/lib/examples/hot_reload.dart | 2 +- .../example/lib/examples/inherited_value.dart | 2 +- .../example/lib/examples/signal_provider.dart | 8 +- .../example/lib/examples/unified.dart | 110 +++++++++++++++ packages/signals_flutter/example/pubspec.lock | 37 ++--- .../lib/src/watch/builder.dart | 127 ++++++++++++++++++ .../signals_flutter/lib/src/watch/watch.dart | 1 + .../signals_flutter/lib/src/watch/widget.dart | 76 ++--------- packages/signals_flutter/pubspec.yaml | 4 +- .../signals_flutter/test/utils/counter.dart | 4 +- 33 files changed, 344 insertions(+), 159 deletions(-) create mode 100644 packages/signals_flutter/example/lib/examples/unified.dart create mode 100644 packages/signals_flutter/lib/src/watch/builder.dart diff --git a/benchmark/pubspec.lock b/benchmark/pubspec.lock index c8846d0f..4621049b 100644 --- a/benchmark/pubspec.lock +++ b/benchmark/pubspec.lock @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.18.0" fake_async: dependency: transitive description: @@ -103,18 +103,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -187,34 +187,40 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + preact_signals: + dependency: transitive + description: + name: preact_signals + sha256: a5b445796a02244b85fa43d79a7030fbfbbb08f7c48277ee93c636140a8fb2e0 + url: "https://pub.dev" + source: hosted + version: "1.8.3" signals: dependency: "direct main" description: path: "../packages/signals" relative: true source: path - version: "5.5.0" + version: "6.0.0-rc.3" signals_core: - dependency: transitive + dependency: "direct overridden" description: - name: signals_core - sha256: f4af1dd285e89bf0bb268676c9d175ea6fb6a894d81260f6b564decad46ad240 - url: "https://pub.dev" - source: hosted - version: "5.5.0" + path: "../packages/signals_core" + relative: true + source: path + version: "6.0.0-rc.3" signals_flutter: - dependency: transitive + dependency: "direct overridden" description: - name: signals_flutter - sha256: "2d9678e39d698ca633c8609872354797511f2af33b4d4ee7b6bfeb487aad6258" - url: "https://pub.dev" - source: hosted - version: "5.5.0" + path: "../packages/signals_flutter" + relative: true + source: path + version: "6.0.0-rc.3" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.0" + version: "0.0.99" solidart: dependency: "direct main" description: @@ -267,10 +273,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.2.0" term_glyph: dependency: transitive description: @@ -283,10 +289,10 @@ packages: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.2" vector_math: dependency: transitive description: @@ -304,5 +310,5 @@ packages: source: hosted version: "14.2.5" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.19.0" diff --git a/examples/animations_example/lib/main.dart b/examples/animations_example/lib/main.dart index 6484af7f..3fe5643c 100644 --- a/examples/animations_example/lib/main.dart +++ b/examples/animations_example/lib/main.dart @@ -83,7 +83,7 @@ class _MyHomePageState extends State { body: Stack( children: [ for (final rect in circles) - Watch((context, _) { + Watch((context) { return AnimatedPositioned.fromRect( rect: rect.value!, duration: kThemeAnimationDuration, diff --git a/examples/auth_flow/lib/main.dart b/examples/auth_flow/lib/main.dart index 12e233d6..da3fa1ac 100644 --- a/examples/auth_flow/lib/main.dart +++ b/examples/auth_flow/lib/main.dart @@ -148,7 +148,7 @@ class DarkModeToggle extends StatelessWidget { @override Widget build(BuildContext context) { - return Watch((_, __) { + return Watch((_) { final isDark = brightness() == Brightness.dark; return IconButton( onPressed: () { @@ -195,7 +195,7 @@ class _HomeScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Count value:'), - Watch((context, _) { + Watch((context) { return Text( '$counter', style: Theme.of(context).textTheme.headlineMedium, @@ -288,7 +288,7 @@ class ProfileScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return Watch((context, _) { + return Watch((context) { return Scaffold( appBar: AppBar( title: Text('Profile: ${auth.currentUserName()}'), diff --git a/examples/clean_architecture/lib/presentation/views/edit_todo.dart b/examples/clean_architecture/lib/presentation/views/edit_todo.dart index 7b01cc45..93d613ed 100644 --- a/examples/clean_architecture/lib/presentation/views/edit_todo.dart +++ b/examples/clean_architecture/lib/presentation/views/edit_todo.dart @@ -67,7 +67,7 @@ class _EditTodoState extends State { ), const SizedBox(height: 16), Watch( - (context, _) => CheckboxListTile( + (context) => CheckboxListTile( title: const Text('Completed'), value: completed.value, onChanged: (val) => completed.value = val!, diff --git a/examples/clean_architecture/lib/presentation/views/todo_list.dart b/examples/clean_architecture/lib/presentation/views/todo_list.dart index f2592780..dfc60ac3 100644 --- a/examples/clean_architecture/lib/presentation/views/todo_list.dart +++ b/examples/clean_architecture/lib/presentation/views/todo_list.dart @@ -40,7 +40,7 @@ class _TodoListViewState extends State { ), ], ), - body: Watch((context, _) { + body: Watch((context) { if (widget.viewModel.todos.isEmpty) { return const Center( child: Text('No todos'), diff --git a/examples/crud_dio/lib/ui/home.dart b/examples/crud_dio/lib/ui/home.dart index 0a4c45e0..09f1cceb 100644 --- a/examples/crud_dio/lib/ui/home.dart +++ b/examples/crud_dio/lib/ui/home.dart @@ -22,7 +22,7 @@ class _MyHomePageState extends State { backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), - body: Watch((context, _) { + body: Watch((context) { final posts = postsService.posts; if (posts.value.isLoading) { return const Center( diff --git a/examples/crud_dio/lib/ui/home_detail.dart b/examples/crud_dio/lib/ui/home_detail.dart index 1451e632..ffcf51c1 100644 --- a/examples/crud_dio/lib/ui/home_detail.dart +++ b/examples/crud_dio/lib/ui/home_detail.dart @@ -45,7 +45,7 @@ class _DetailPostState extends State { @override Widget build(BuildContext context) { - return Watch((context, _) { + return Watch((context) { final post = postsService.post.value; return Scaffold( appBar: AppBar( diff --git a/examples/drift_example/lib/screens/home/drawer.dart b/examples/drift_example/lib/screens/home/drawer.dart index 07ebfbdb..0f59e342 100644 --- a/examples/drift_example/lib/screens/home/drawer.dart +++ b/examples/drift_example/lib/screens/home/drawer.dart @@ -33,7 +33,7 @@ class _CategoriesDrawerState extends State { ), ), Flexible( - child: Watch((context, _) { + child: Watch((context) { final value = stream.value.value; final categories = value ?? []; return ListView.builder( diff --git a/examples/eval_calculator/lib/screens/spreadsheet.dart b/examples/eval_calculator/lib/screens/spreadsheet.dart index c597ea85..3f406760 100644 --- a/examples/eval_calculator/lib/screens/spreadsheet.dart +++ b/examples/eval_calculator/lib/screens/spreadsheet.dart @@ -28,7 +28,7 @@ class _SpreadsheetCalculatorState extends State { Widget valueToWidget(ValueRow valueRow, BuildContext context) { final valueDisplay = Watch( - (context, _) => Text(valueRow.signalObject.value.toString(), style: _style), + (context) => Text(valueRow.signalObject.value.toString(), style: _style), ); if (valueRow.type == ValueType.number) { @@ -59,7 +59,7 @@ class _SpreadsheetCalculatorState extends State { @override Widget build(BuildContext context) { const cellSize = 30.0; - return Watch((context, _) { + return Watch((context) { final selected = selectedCells(); return Scaffold( appBar: AppBar( diff --git a/examples/eval_calculator/lib/screens/table.dart b/examples/eval_calculator/lib/screens/table.dart index e97c191f..4a81771f 100644 --- a/examples/eval_calculator/lib/screens/table.dart +++ b/examples/eval_calculator/lib/screens/table.dart @@ -51,7 +51,7 @@ class _TextTableState extends State { Widget valueToWidget(ValueRow valueRow, BuildContext context) { final valueDisplay = Watch( - (context, _) => Text(valueRow.signalObject.value.toString(), style: _style), + (context) => Text(valueRow.signalObject.value.toString(), style: _style), ); if (valueRow.type == ValueType.number) { @@ -89,7 +89,7 @@ class _TextTableState extends State { @override Widget build(BuildContext context) { - return Watch((context, _) { + return Watch((context) { return Padding( padding: const EdgeInsets.all(20), child: ListView( diff --git a/examples/flutter_async/lib/main.dart b/examples/flutter_async/lib/main.dart index 09a4d1a1..e742ffee 100644 --- a/examples/flutter_async/lib/main.dart +++ b/examples/flutter_async/lib/main.dart @@ -59,14 +59,14 @@ class _ExampleState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Watch((_, __) { + Watch((_) { return SwitchListTile( title: const Text('Throw Error'), value: throwError.value, onChanged: (value) => throwError.value = value, ); }), - Watch((context, _) { + Watch((context) { return ListTile( title: const Text('Result (map)'), subtitle: _futureSignal.value.map( @@ -103,7 +103,7 @@ class _ExampleState extends State { ), ); }), - Watch((context, _) { + Watch((context) { return ListTile( title: const Text('Result (switch)'), subtitle: switch (_futureSignal.value) { diff --git a/examples/flutter_colorband/lib/color_scale.dart b/examples/flutter_colorband/lib/color_scale.dart index 2dcfd484..989c68fe 100644 --- a/examples/flutter_colorband/lib/color_scale.dart +++ b/examples/flutter_colorband/lib/color_scale.dart @@ -22,7 +22,7 @@ class ColorScale extends StatelessWidget { @override build(context) { - return Watch((context, _) { + return Watch((context) { final conf = signals.confSignal.value; final configValueStyle = TextStyle( diff --git a/examples/flutter_colorband/lib/color_scale_band.dart b/examples/flutter_colorband/lib/color_scale_band.dart index 4df636d0..82cef31e 100644 --- a/examples/flutter_colorband/lib/color_scale_band.dart +++ b/examples/flutter_colorband/lib/color_scale_band.dart @@ -9,7 +9,7 @@ class ColorBandDisplay extends StatelessWidget { @override build(context) { - return Watch((builder, _) { + return Watch((builder) { final conf = signals.confSignal.value; final band = signals.bandSignal.value; diff --git a/examples/flutter_colorband/lib/color_scale_selector.dart b/examples/flutter_colorband/lib/color_scale_selector.dart index 72b748f3..40a72004 100644 --- a/examples/flutter_colorband/lib/color_scale_selector.dart +++ b/examples/flutter_colorband/lib/color_scale_selector.dart @@ -11,7 +11,7 @@ class ColorName extends StatelessWidget { @override build(context) { - return Watch((context, _) { + return Watch((context) { final color = signals.colorNameSignal.value; final conf = signals.confSignal.value; @@ -49,7 +49,7 @@ class ColorNameStream extends StatelessWidget { @override build(context) => Watch( - (context, _) { + (context) { return signals.colorNameSignalStream.value.map( data: (name) => Semantics( label: 'Current color', @@ -79,7 +79,7 @@ class ColorSelector extends StatelessWidget { {super.key, required this.conf, required this.valueStyle}); @override - build(context) => Watch((context, _) { + build(context) => Watch((context) { final band = signals.bandSignal.value; return SizedBox.expand( diff --git a/examples/get_it_signals/lib/main.dart b/examples/get_it_signals/lib/main.dart index a97bc0fc..dc86d62a 100644 --- a/examples/get_it_signals/lib/main.dart +++ b/examples/get_it_signals/lib/main.dart @@ -145,7 +145,7 @@ class DarkModeToggle extends StatelessWidget { @override Widget build(BuildContext context) { - return Watch((_, __) { + return Watch((_) { final isDark = brightness() == Brightness.dark; return IconButton( onPressed: () { @@ -192,7 +192,7 @@ class _HomeScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Count value:'), - Watch((context, _) { + Watch((context) { return Text( '$counter', style: Theme.of(context).textTheme.headlineMedium, @@ -303,7 +303,7 @@ class ProfileScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Profile'), - Watch((context, _) { + Watch((context) { return Text( getIt.get().currentUserName(), style: Theme.of(context).textTheme.headlineMedium, diff --git a/examples/infinite_scroll/lib/main.dart b/examples/infinite_scroll/lib/main.dart index 162cf5ad..66c82d58 100644 --- a/examples/infinite_scroll/lib/main.dart +++ b/examples/infinite_scroll/lib/main.dart @@ -111,7 +111,7 @@ class InfiniteScroll extends StatelessWidget { title: const Text('Infinite Scroll'), ), body: Watch.builder( - builder: (context, _) { + builder: (context) { final items = source.value.value ?? []; final more = source.hasMore(); if (items.isEmpty) { diff --git a/examples/node_based_editor/lib/editor.dart b/examples/node_based_editor/lib/editor.dart index 83a90b9a..37315603 100644 --- a/examples/node_based_editor/lib/editor.dart +++ b/examples/node_based_editor/lib/editor.dart @@ -66,14 +66,14 @@ class _EditorState extends State { ..scale(0.9, 0.9) ..translate(-(size.width / 2), -(size.height / 2))), ), - Watch((context, _) { + Watch((context) { return IconButton( onPressed: () => toolbox.set(!toolbox()), icon: Icon(toolbox() ? Icons.visibility_off : Icons.visibility), tooltip: toolbox() ? 'Hide Toolbox' : 'Show Toolbox', ); }), - Watch((context, _) { + Watch((context) { if (selection.isEmpty) return const SizedBox.shrink(); return IconButton( onPressed: () => selection.clear(), @@ -81,7 +81,7 @@ class _EditorState extends State { tooltip: 'Clear selection', ); }), - Watch((context, _) { + Watch((context) { if (nodes.isEmpty) return const SizedBox.shrink(); return IconButton( onPressed: () { @@ -95,7 +95,7 @@ class _EditorState extends State { ], ), body: Watch.builder( - builder: (context, _) => Stack( + builder: (context) => Stack( children: [ Positioned.fill( child: CodeView( diff --git a/examples/node_based_editor/lib/nodes/datetime.dart b/examples/node_based_editor/lib/nodes/datetime.dart index c4f20982..0efaf03b 100644 --- a/examples/node_based_editor/lib/nodes/datetime.dart +++ b/examples/node_based_editor/lib/nodes/datetime.dart @@ -24,7 +24,7 @@ class DateTimeNode extends ValueNode { @override Widget build() { if (output is Signal) { - return Watch.builder(builder: (context, _) { + return Watch.builder(builder: (context) { return InkWell( onTap: () { final now = DateTime.now(); diff --git a/examples/node_based_editor/lib/nodes/time.dart b/examples/node_based_editor/lib/nodes/time.dart index 787d3fee..d115a22a 100644 --- a/examples/node_based_editor/lib/nodes/time.dart +++ b/examples/node_based_editor/lib/nodes/time.dart @@ -20,7 +20,7 @@ class TimeOfDayNode extends ValueNode { }) : super.computed(); @override - Widget build() => Watch.builder(builder: (context, _) { + Widget build() => Watch.builder(builder: (context) { if (output is Signal) { return InkWell( onTap: () { diff --git a/packages/signals/pubspec.yaml b/packages/signals/pubspec.yaml index 87779ae8..73e56f78 100644 --- a/packages/signals/pubspec.yaml +++ b/packages/signals/pubspec.yaml @@ -3,7 +3,7 @@ description: "Reactivity made simple. Do more by doing less. Supports Flutter an repository: https://github.com/rodydavis/signals.dart homepage: https://dartsignals.dev/ documentation: https://dartsignals.dev/ -version: 6.0.0-rc.3 +version: 6.0.0-rc.4 environment: sdk: ">=3.0.0 <4.0.0" @@ -12,8 +12,8 @@ environment: dependencies: flutter: sdk: flutter - signals_core: ^6.0.0-rc.3 - signals_flutter: ^6.0.0-rc.3 + signals_core: ^6.0.0-rc.4 + signals_flutter: ^6.0.0-rc.4 dev_dependencies: flutter_test: diff --git a/packages/signals_core/pubspec.yaml b/packages/signals_core/pubspec.yaml index 975d91da..5c3d2fe8 100644 --- a/packages/signals_core/pubspec.yaml +++ b/packages/signals_core/pubspec.yaml @@ -1,6 +1,6 @@ name: signals_core description: The signals library exposes four core functions which are the building blocks to model any business logic you can think of. -version: 6.0.0-rc.3 +version: 6.0.0-rc.4 repository: https://github.com/rodydavis/signals.dart homepage: https://dartsignals.dev/ documentation: https://dartsignals.dev/ @@ -14,7 +14,7 @@ dependencies: dev_dependencies: coverage: ^1.7.2 - lints: ^5.1.0 + lints: ^5.0.0 test: ^1.24.0 topics: diff --git a/packages/signals_devtools_extension/lib/src/node_table.dart b/packages/signals_devtools_extension/lib/src/node_table.dart index abea2b4e..4bb39307 100644 --- a/packages/signals_devtools_extension/lib/src/node_table.dart +++ b/packages/signals_devtools_extension/lib/src/node_table.dart @@ -7,7 +7,7 @@ class NodesTable extends StatelessWidget { const NodesTable({super.key}); @override Widget build(BuildContext context) { - return Watch((context, _) { + return Watch((context) { if (nodes.isEmpty) { return const Center(child: Text('No nodes created')); } diff --git a/packages/signals_flutter/example/lib/examples/computed_reorder.dart b/packages/signals_flutter/example/lib/examples/computed_reorder.dart index 1a7f4a09..317b1096 100644 --- a/packages/signals_flutter/example/lib/examples/computed_reorder.dart +++ b/packages/signals_flutter/example/lib/examples/computed_reorder.dart @@ -107,7 +107,7 @@ class _ReorderableItemListState extends State { child: const Text('Switch Category'), ), Expanded( - child: Watch((context, _) { + child: Watch((context) { final items = filteredAndSortedItems.value; return ReorderableListView.builder( itemCount: items.length, diff --git a/packages/signals_flutter/example/lib/examples/hot_reload.dart b/packages/signals_flutter/example/lib/examples/hot_reload.dart index 788f1687..dd1c08ab 100644 --- a/packages/signals_flutter/example/lib/examples/hot_reload.dart +++ b/packages/signals_flutter/example/lib/examples/hot_reload.dart @@ -19,7 +19,7 @@ class _MainAppState extends State { Widget build(BuildContext context) { return MaterialApp( home: Watch( - (_, __) { + (_) { final backgroundColor = backgroundColorSignal.value; return Scaffold( diff --git a/packages/signals_flutter/example/lib/examples/inherited_value.dart b/packages/signals_flutter/example/lib/examples/inherited_value.dart index eba659b6..e1dfc15c 100644 --- a/packages/signals_flutter/example/lib/examples/inherited_value.dart +++ b/packages/signals_flutter/example/lib/examples/inherited_value.dart @@ -65,7 +65,7 @@ class Example extends StatelessWidget { @override Widget build(BuildContext context) { final value = InheritedValue.of(context); - return Watch((context, _) { + return Watch((context) { final other = signal$.value; return Text('value: $value, other: $other'); }); diff --git a/packages/signals_flutter/example/lib/examples/signal_provider.dart b/packages/signals_flutter/example/lib/examples/signal_provider.dart index 90754a1b..f04a3de5 100644 --- a/packages/signals_flutter/example/lib/examples/signal_provider.dart +++ b/packages/signals_flutter/example/lib/examples/signal_provider.dart @@ -15,7 +15,7 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, theme: ThemeData.light(), darkTheme: ThemeData.dark(), - home: const SignalProviderExample(), + home: const Example(), ); } } @@ -26,10 +26,10 @@ class Counter extends FlutterSignal { void increment() => value++; } -class SignalProviderExample extends StatelessWidget { - const SignalProviderExample({ +class Example extends StatelessWidget { + const Example({ super.key, - this.title = 'SignalProvider Example', + this.title = 'Example', }); final String title; diff --git a/packages/signals_flutter/example/lib/examples/unified.dart b/packages/signals_flutter/example/lib/examples/unified.dart new file mode 100644 index 00000000..c0378d2c --- /dev/null +++ b/packages/signals_flutter/example/lib/examples/unified.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:signals_flutter/signals_flutter.dart'; + +final counter = Counter(0); + +void main(List args) { + runApp(App(count: counter)); +} + +class App extends StatelessWidget { + const App({super.key, required this.count}); + + final Counter count; + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + themeMode: ThemeMode.system, + home: Example(count: count), + ); + } +} + +class Example extends StatelessWidget { + const Example({ + super.key, + required this.count, + }); + + final Counter count; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Unified Signal Example'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'You have pushed the button this many times:', + ), + Builder(builder: (context) { + return Text( + '${count.watch(context)}', + style: Theme.of(context).textTheme.titleLarge, + ); + }), + StreamBuilder( + stream: count, + builder: (context, snapshot) { + return Text( + 'Stream: ${snapshot.data}', + style: Theme.of(context).textTheme.titleLarge, + ); + }, + ), + ValueListenableBuilder( + valueListenable: count, + builder: (context, value, child) { + return Text( + 'ValueNotifier: $value', + style: Theme.of(context).textTheme.titleLarge, + ); + }, + ), + Watch.builder( + builder: (context) { + return Text( + 'Watch: ${count.value}', + style: Theme.of(context).textTheme.titleLarge, + ); + }, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: count.increment, + child: const Text('Increment'), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: count.decrement, + child: const Text('Decrement'), + ), + ], + ), + ], + ), + ), + ); + } +} + +class UnifiedSignal extends Signal + with ValueNotifierSignalMixin, StreamSignalMixin, SinkSignalMixin { + UnifiedSignal(super.internalValue); +} + +class Counter extends UnifiedSignal { + Counter([super.internalValue = 0]); + void increment() => value++; + void decrement() => value--; +} diff --git a/packages/signals_flutter/example/pubspec.lock b/packages/signals_flutter/example/pubspec.lock index a5b67719..9bdbcc58 100644 --- a/packages/signals_flutter/example/pubspec.lock +++ b/packages/signals_flutter/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.18.0" fake_async: dependency: transitive description: @@ -71,18 +71,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -132,11 +132,12 @@ packages: source: hosted version: "1.9.0" preact_signals: - dependency: "direct overridden" + dependency: transitive description: - path: "../../preact_signals" - relative: true - source: path + name: preact_signals + sha256: a5b445796a02244b85fa43d79a7030fbfbbb08f7c48277ee93c636140a8fb2e0 + url: "https://pub.dev" + source: hosted version: "1.8.3" signals_core: dependency: "direct overridden" @@ -144,19 +145,19 @@ packages: path: "../../signals_core" relative: true source: path - version: "6.0.0-rc.3" + version: "6.0.0-rc.4" signals_flutter: dependency: "direct main" description: path: ".." relative: true source: path - version: "6.0.0-rc.3" + version: "6.0.0-rc.4" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.0" + version: "0.0.99" source_span: dependency: transitive description: @@ -185,10 +186,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.2.0" term_glyph: dependency: transitive description: @@ -201,10 +202,10 @@ packages: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.2" vector_math: dependency: transitive description: @@ -222,5 +223,5 @@ packages: source: hosted version: "14.2.5" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/packages/signals_flutter/lib/src/watch/builder.dart b/packages/signals_flutter/lib/src/watch/builder.dart new file mode 100644 index 00000000..6c099ac2 --- /dev/null +++ b/packages/signals_flutter/lib/src/watch/builder.dart @@ -0,0 +1,127 @@ +part of 'watch.dart'; + +/// ## Watch +/// +/// To watch a signal for changes in Flutter, use the `Watch` widget. This will only rebuild this widget method and not the entire widget tree. +/// +/// ```dart +/// final signal = signal(10); +/// ... +/// @override +/// Widget build(BuildContext context) { +/// return Watch((context, _) => Text('$signal')); +/// } +/// ``` +/// +/// This will also automatically unsubscribe when the widget is disposed. +/// +/// Any inherited widgets referenced to inside the Watch scope will be subscribed to for updates ([MediaQuery](https://api.flutter.dev/flutter/widgets/MediaQuery-class.html), [Theme](https://api.flutter.dev/flutter/material/Theme-class.html), etc.) and retrigger the builder method. +/// +/// There is also a drop in replacement for builder: +/// +/// ```diff +/// final signal = signal(10); +/// ... +/// @override +/// Widget build(BuildContext context) { +/// - return Builder( +/// + return Watch.builder( +/// builder: (context, _) => Text('$signal'), +/// ); +/// } +/// ``` +class WatchBuilder extends StatefulWidget { + /// Minimal builder for signal changes that rerender a widget tree. + /// + /// ```dart + /// final counter = signal(0); + /// ... + /// Watch((context) => Text('$counter')) + /// ``` + const WatchBuilder({ + super.key, + required this.builder, + this.debugLabel, + this.dependencies = const [], + this.child, + }); + + /// The widget to rebuild when any signals change + final T Function(BuildContext context, Widget? child) builder; + + /// Optional debug label to use for devtools + final String? debugLabel; + + /// Cached widget to pass in + final Widget? child; + + /// List of optional dependencies to watch + final List> dependencies; + + @override + State> createState() => _WatchState(); +} + +class _WatchState extends State> + with SignalsMixin { + late final result = createComputed(() { + return widget.builder(context, widget.child); + }, debugLabel: widget.debugLabel); + bool _init = true; + + @override + void initState() { + super.initState(); + for (final dep in widget.dependencies) { + bindSignal(dep); + } + } + + // coverage:ignore-start + @override + void reassemble() { + super.reassemble(); + final target = core.SignalsObserver.instance; + if (target is core.DevToolsSignalsObserver) { + target.reassemble(); + } + WidgetsBinding.instance.addPostFrameCallback((_) { + result.recompute(); + if (mounted) setState(() {}); + result.value; + }); + } + // coverage:ignore-end + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_init) { + // Called on first build (we do not need to rebuild yet) + _init = false; + return; + } + result.recompute(); + } + + @override + void didUpdateWidget(covariant WatchBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.dependencies != widget.dependencies) { + for (final dep in oldWidget.dependencies) { + final idx = widget.dependencies.indexOf(dep); + if (idx == -1) unbindSignal(dep); + } + for (final dep in widget.dependencies) { + bindSignal(dep); + } + } else if (oldWidget.builder != widget.builder) { + result.recompute(); + } + } + + @override + Widget build(BuildContext context) { + return result.value; + } +} diff --git a/packages/signals_flutter/lib/src/watch/watch.dart b/packages/signals_flutter/lib/src/watch/watch.dart index 57dc3345..4d15fc22 100644 --- a/packages/signals_flutter/lib/src/watch/watch.dart +++ b/packages/signals_flutter/lib/src/watch/watch.dart @@ -7,3 +7,4 @@ import '../mixins/signals.dart'; part 'widget.dart'; part 'element_watcher.dart'; part 'extension.dart'; +part 'builder.dart'; diff --git a/packages/signals_flutter/lib/src/watch/widget.dart b/packages/signals_flutter/lib/src/watch/widget.dart index fa811e18..cfee588a 100644 --- a/packages/signals_flutter/lib/src/watch/widget.dart +++ b/packages/signals_flutter/lib/src/watch/widget.dart @@ -30,7 +30,7 @@ part of 'watch.dart'; /// ); /// } /// ``` -class Watch extends StatefulWidget { +class Watch extends StatelessWidget { /// Minimal builder for signal changes that rerender a widget tree. /// /// ```dart @@ -43,7 +43,6 @@ class Watch extends StatefulWidget { super.key, this.debugLabel, this.dependencies = const [], - this.child, }); /// Drop in replacement for the Flutter builder widget. @@ -63,83 +62,24 @@ class Watch extends StatefulWidget { required this.builder, this.debugLabel, this.dependencies = const [], - this.child, }); /// The widget to rebuild when any signals change - final T Function(BuildContext context, Widget? child) builder; + final T Function(BuildContext context) builder; /// Optional debug label to use for devtools final String? debugLabel; - /// Cached widget to pass in - final Widget? child; - /// List of optional dependencies to watch final List> dependencies; - @override - State> createState() => _WatchState(); -} - -class _WatchState extends State> with SignalsMixin { - late final result = createComputed(() { - return widget.builder(context, widget.child); - }, debugLabel: widget.debugLabel); - bool _init = true; - - @override - void initState() { - super.initState(); - for (final dep in widget.dependencies) { - bindSignal(dep); - } - } - - // coverage:ignore-start - @override - void reassemble() { - super.reassemble(); - final target = core.SignalsObserver.instance; - if (target is core.DevToolsSignalsObserver) { - target.reassemble(); - } - WidgetsBinding.instance.addPostFrameCallback((_) { - result.recompute(); - if (mounted) setState(() {}); - }); - } - // coverage:ignore-end - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (_init) { - // Called on first build (we do not need to rebuild yet) - _init = false; - return; - } - result.recompute(); - } - - @override - void didUpdateWidget(covariant Watch oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.dependencies != widget.dependencies) { - for (final dep in oldWidget.dependencies) { - final idx = widget.dependencies.indexOf(dep); - if (idx == -1) unbindSignal(dep); - } - for (final dep in widget.dependencies) { - bindSignal(dep); - } - } else if (oldWidget.builder != widget.builder) { - result.recompute(); - } - } - @override Widget build(BuildContext context) { - return result.value; + return WatchBuilder( + // key: key, + builder: (context, _) => builder(context), + debugLabel: debugLabel, + dependencies: dependencies, + ); } } diff --git a/packages/signals_flutter/pubspec.yaml b/packages/signals_flutter/pubspec.yaml index ac2a71d8..cfcf4185 100644 --- a/packages/signals_flutter/pubspec.yaml +++ b/packages/signals_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: signals_flutter description: The signals library exposes four core functions which are the building blocks to model any business logic you can think of. -version: 6.0.0-rc.3 +version: 6.0.0-rc.4 repository: https://github.com/rodydavis/signals.dart homepage: https://dartsignals.dev/ documentation: https://dartsignals.dev/ @@ -12,7 +12,7 @@ environment: dependencies: flutter: sdk: flutter - signals_core: ^6.0.0-rc.3 + signals_core: ^6.0.0-rc.4 dev_dependencies: flutter_test: diff --git a/packages/signals_flutter/test/utils/counter.dart b/packages/signals_flutter/test/utils/counter.dart index 3b483d97..60adce08 100644 --- a/packages/signals_flutter/test/utils/counter.dart +++ b/packages/signals_flutter/test/utils/counter.dart @@ -45,12 +45,12 @@ class CounterState extends State { body: () { if (widget.watch) { if (widget.builder) { - return Watch.builder(builder: (context, _) { + return Watch.builder(builder: (context) { widget.callback(); return Text('Count: $display'); }); } else { - return Watch((context, _) { + return Watch((context) { widget.callback(); return Text('Count: $display'); });