From 41d89ca710418321704197bef180c5c7d7b0be05 Mon Sep 17 00:00:00 2001 From: Jacob Moura Date: Fri, 10 Dec 2021 11:39:29 -0300 Subject: [PATCH 1/9] Added Modular Watch Extension --- .../src/presenter/widgets/modular_app.dart | 118 +++++++++++++++++- 1 file changed, 113 insertions(+), 5 deletions(-) diff --git a/flutter_modular/lib/src/presenter/widgets/modular_app.dart b/flutter_modular/lib/src/presenter/widgets/modular_app.dart index 0b5d223f..9c7cab4b 100644 --- a/flutter_modular/lib/src/presenter/widgets/modular_app.dart +++ b/flutter_modular/lib/src/presenter/widgets/modular_app.dart @@ -26,8 +26,7 @@ class ModularApp extends StatefulWidget { /// Prohibits taking any bind of parent modules, forcing the imports of the same in the current module to be accessed. This is the same behavior as the system. Default is false; bool notAllowedParentBinds = false, }) : super(key: key) { - (Modular as ModularBase).flags.experimentalNotAllowedParentBinds = - notAllowedParentBinds; + (Modular as ModularBase).flags.experimentalNotAllowedParentBinds = notAllowedParentBinds; (Modular as ModularBase).flags.isDebug = debugMode; } @@ -51,8 +50,7 @@ class ModularAppState extends State { @override void dispose() { Modular.destroy(); - Modular.debugPrintModular( - '-- ${widget.module.runtimeType.toString()} DISPOSED'); + Modular.debugPrintModular('-- ${widget.module.runtimeType.toString()} DISPOSED'); cleanGlobals(); super.dispose(); } @@ -65,6 +63,116 @@ class ModularAppState extends State { @override Widget build(BuildContext context) { - return widget.child; + return _ModularInherited(child: widget.child); + } +} + +typedef SelectCallback = Function(T bind); + +class _Register { + final T value; + Type get type => T; + final SelectCallback? _select; + + _Register(this.value, this._select); + + dynamic getSelected() => _select != null ? _select!(value) : value; + + @override + bool operator ==(Object object) => identical(this, object) || object is _Register && runtimeType == object.runtimeType && type == object.type; + + @override + int get hashCode => value.hashCode ^ type.hashCode; +} + +class _ModularInherited extends InheritedWidget { + const _ModularInherited({Key? key, required Widget child}) : super(key: key, child: child); + + static T of(BuildContext context, {bool listen = true, SelectCallback? select}) { + final bind = Modular.get(); + if (listen) { + final registre = _Register(bind, select); + context.dependOnInheritedWidgetOfExactType<_ModularInherited>(aspect: registre)!; + } + + return bind; + } + + @override + bool updateShouldNotify(covariant InheritedWidget oldWidget) { + return false; + } + + @override + InheritedElement createElement() => _InheritedModularElement(this); +} + +class _InheritedModularElement extends InheritedElement { + _InheritedModularElement(InheritedWidget widget) : super(widget); + + bool _dirty = false; + + Type? current; + + @override + void updateDependencies(Element dependent, covariant _Register aspect) { + final localRegister = getDependencies(dependent) as _Register?; + + if (localRegister == aspect.value) { + return; + } + + final value = aspect.getSelected(); + + if (value is Listenable) { + value.addListener(() => _handleUpdate(aspect.type)); + } else if (value is Stream) { + value.listen((event) => _handleUpdate(aspect.type)); + } else if (value is Store) { + value.observer( + onState: (state) => _handleUpdate(aspect.type), + onError: (error) => _handleUpdate(aspect.type), + onLoading: (isLoading) => _handleUpdate(aspect.type), + ); + } + setDependencies(dependent, aspect); + } + + @override + Widget build() { + if (_dirty) notifyClients(widget); + return super.build(); + } + + void _handleUpdate(Type type) { + current = type; + _dirty = true; + markNeedsBuild(); + } + + @override + void notifyClients(InheritedWidget oldWidget) { + super.notifyClients(oldWidget); + _dirty = false; + current = null; + } + + @override + void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) { + final register = getDependencies(dependent) as _Register; + + if (register.type == current) { + dependent.didChangeDependencies(); + } + } +} + +extension ModularWatchExtension on BuildContext { + T watch([SelectCallback? select]) { + return _ModularInherited.of(this, select: select); + } + + T read() { + return _ModularInherited.of(this, listen: false); } } From 8cc060bcc4df0e331253a4f1441efb4e422e28ef Mon Sep 17 00:00:00 2001 From: Jacob Moura Date: Fri, 10 Dec 2021 15:46:49 -0300 Subject: [PATCH 2/9] added tests --- .../src/presenter/widgets/modular_app.dart | 21 ++++-- .../presenter/widgets/modular_app_test.dart | 73 +++++++++++++++++-- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/flutter_modular/lib/src/presenter/widgets/modular_app.dart b/flutter_modular/lib/src/presenter/widgets/modular_app.dart index 9c7cab4b..fa80323a 100644 --- a/flutter_modular/lib/src/presenter/widgets/modular_app.dart +++ b/flutter_modular/lib/src/presenter/widgets/modular_app.dart @@ -92,7 +92,8 @@ class _ModularInherited extends InheritedWidget { final bind = Modular.get(); if (listen) { final registre = _Register(bind, select); - context.dependOnInheritedWidgetOfExactType<_ModularInherited>(aspect: registre)!; + final inherited = context.dependOnInheritedWidgetOfExactType<_ModularInherited>(aspect: registre)!; + inherited.updateShouldNotify(inherited); } return bind; @@ -116,9 +117,11 @@ class _InheritedModularElement extends InheritedElement { @override void updateDependencies(Element dependent, covariant _Register aspect) { - final localRegister = getDependencies(dependent) as _Register?; + var registers = getDependencies(dependent) as Set<_Register>?; - if (localRegister == aspect.value) { + registers ??= {}; + + if (registers.contains(aspect)) { return; } @@ -135,7 +138,8 @@ class _InheritedModularElement extends InheritedElement { onLoading: (isLoading) => _handleUpdate(aspect.type), ); } - setDependencies(dependent, aspect); + registers.add(aspect); + setDependencies(dependent, registers); } @override @@ -159,10 +163,13 @@ class _InheritedModularElement extends InheritedElement { @override void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) { - final register = getDependencies(dependent) as _Register; + var registers = getDependencies(dependent) as Set<_Register>?; + registers ??= {}; - if (register.type == current) { - dependent.didChangeDependencies(); + for (var register in registers) { + if (register.type == current) { + dependent.didChangeDependencies(); + } } } } diff --git a/flutter_modular/test/src/presenter/widgets/modular_app_test.dart b/flutter_modular/test/src/presenter/widgets/modular_app_test.dart index 39382f3c..ea5c91f9 100644 --- a/flutter_modular/test/src/presenter/widgets/modular_app_test.dart +++ b/flutter_modular/test/src/presenter/widgets/modular_app_test.dart @@ -1,22 +1,34 @@ import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:triple/triple.dart'; void main() { testWidgets('ModularApp', (tester) async { final modularKey = UniqueKey(); - final modularApp = - ModularApp(key: modularKey, module: CustomModule(), child: AppWidget()); + final modularApp = ModularApp(key: modularKey, module: CustomModule(), child: AppWidget()); await tester.pumpWidget(modularApp); await tester.pump(); - final finder = find.byKey(key); - expect(finder, findsOneWidget); + expect(find.byKey(key), findsOneWidget); final state = tester.state(find.byKey(modularKey)); final result = state.tripleResolverCallback(); state.reassemble(); expect(result, 'test'); + + await tester.pump(); + final notifier = state.tripleResolverCallback>(); + notifier.value++; + + await tester.pump(); + + expect(find.text('1'), findsOneWidget); + + final store = state.tripleResolverCallback(); + store.update(1); + + // await tester.pump(); }); } @@ -24,11 +36,16 @@ final key = UniqueKey(); class CustomModule extends Module { @override - List get binds => [Bind.factory((i) => 'test')]; + List get binds => [ + Bind.factory((i) => 'test'), + Bind.singleton((i) => ValueNotifier(0)), + Bind.singleton((i) => Stream.value(0).asBroadcastStream()), + Bind.singleton((i) => MyStore()), + ]; @override List get routes => [ - ChildRoute('/', child: (_, __) => Container(key: key)), + ChildRoute('/', child: (_, __) => Home()), ]; } @@ -37,6 +54,50 @@ class AppWidget extends StatelessWidget { @override Widget build(BuildContext context) { + context.read(); + return MaterialApp().modular(); } } + +class Home extends StatelessWidget { + const Home({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final notifier = context.watch(); + final stream = context.watch(); + final store = context.watch(); + + return Container( + key: key, + child: Text('${notifier.value}'), + ); + } +} + +class MyStore extends Store { + MyStore() : super(0); + + @override + Future destroy() async {} + + late final void Function(int state)? fnState; + late final void Function(bool state)? fnLoading; + late final void Function(Exception state)? fnError; + + @override + void update(int newState, {bool force = false}) { + fnState?.call(newState); + fnError?.call(Exception()); + fnLoading?.call(true); + } + + @override + Disposer observer({void Function(int state)? onState, void Function(bool isLoading)? onLoading, void Function(Exception error)? onError}) { + fnState = onState; + fnLoading = onLoading; + fnError = onError; + return () => Future.value(); + } +} From 8746e2cad77dc46a1459baf0b2723b383015f52d Mon Sep 17 00:00:00 2001 From: Jacob Moura Date: Fri, 10 Dec 2021 15:54:01 -0300 Subject: [PATCH 3/9] added dartdocs --- flutter_modular/lib/src/presenter/widgets/modular_app.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flutter_modular/lib/src/presenter/widgets/modular_app.dart b/flutter_modular/lib/src/presenter/widgets/modular_app.dart index fa80323a..5e8c57ff 100644 --- a/flutter_modular/lib/src/presenter/widgets/modular_app.dart +++ b/flutter_modular/lib/src/presenter/widgets/modular_app.dart @@ -175,10 +175,15 @@ class _InheritedModularElement extends InheritedElement { } extension ModularWatchExtension on BuildContext { + /// Request an instance by [Type] and + /// watch your changes + /// + /// SUPPORTED CLASS ([Listenable], [Stream] and [Store] by Triple). T watch([SelectCallback? select]) { return _ModularInherited.of(this, select: select); } + /// Request an instance by [Type] T read() { return _ModularInherited.of(this, listen: false); } From f36508acb99cec1df5d137aa61c077bd4bf6e2c5 Mon Sep 17 00:00:00 2001 From: Jacob Moura Date: Fri, 10 Dec 2021 16:00:26 -0300 Subject: [PATCH 4/9] update version --- flutter_modular/CHANGELOG.md | 12 ++++++++++++ flutter_modular/pubspec.yaml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/flutter_modular/CHANGELOG.md b/flutter_modular/CHANGELOG.md index 9faaef0e..38edd50f 100644 --- a/flutter_modular/CHANGELOG.md +++ b/flutter_modular/CHANGELOG.md @@ -1,3 +1,15 @@ +## [4.3.0] - 2021-12-10 +* Added BuildContext extension [context.read()] and [context.watch()]; +* The [context.watch()] listen changes of [Listanable], [Stream] and [Store] by Triple; +```dart +class Body extends StatelessWidget { + Widget build(BuildContext context){ + final notifier = context.watch(); + return Text('${notifier.value}') + } +} +``` + ## [4.2.0] - 2021-10-28 * Added cleanInjector() and cleanModular() for restart Modular. [#601](https://github.com/Flutterando/modular/pull/601) * Updated modular_core. diff --git a/flutter_modular/pubspec.yaml b/flutter_modular/pubspec.yaml index cd7f9f75..d795add1 100644 --- a/flutter_modular/pubspec.yaml +++ b/flutter_modular/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_modular description: Smart project structure with dependency injection and route management -version: 4.2.0 +version: 4.3.0 homepage: https://github.com/Flutterando/modular environment: From 9cf8a0bfc7cce36e4e30e694993310540f85f07e Mon Sep 17 00:00:00 2001 From: Jacob Moura Date: Fri, 10 Dec 2021 16:04:41 -0300 Subject: [PATCH 5/9] added examples in changelogs --- flutter_modular/CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/flutter_modular/CHANGELOG.md b/flutter_modular/CHANGELOG.md index 38edd50f..0e50e106 100644 --- a/flutter_modular/CHANGELOG.md +++ b/flutter_modular/CHANGELOG.md @@ -9,6 +9,15 @@ class Body extends StatelessWidget { } } ``` +* Use `select` in `.watch()` to select the reactive property: +```dart +class Body extends StatelessWidget { + Widget build(BuildContext context){ + final bloc = context.watch((bloc) => bloc.stream); + return Text('${bloc.state}') + } +} +``` ## [4.2.0] - 2021-10-28 * Added cleanInjector() and cleanModular() for restart Modular. [#601](https://github.com/Flutterando/modular/pull/601) From acb658f35a20f1acfb0bb78e3d22a5d98f425dcd Mon Sep 17 00:00:00 2001 From: Jacob Moura Date: Fri, 10 Dec 2021 16:10:30 -0300 Subject: [PATCH 6/9] added examples --- flutter_modular/CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/flutter_modular/CHANGELOG.md b/flutter_modular/CHANGELOG.md index 0e50e106..365b47f6 100644 --- a/flutter_modular/CHANGELOG.md +++ b/flutter_modular/CHANGELOG.md @@ -19,6 +19,17 @@ class Body extends StatelessWidget { } ``` +Also, use `Store Selectors` in conjunction with `.watch`: +```dart +class OnlyErrorWidget extends StatelessWidget { + Widget build(BuildContext context){ + // changes with store.setError(); + final store = context.watch((store) => store.selectError); + return Text('${store.error}') + } +} +``` + ## [4.2.0] - 2021-10-28 * Added cleanInjector() and cleanModular() for restart Modular. [#601](https://github.com/Flutterando/modular/pull/601) * Updated modular_core. From 8ab8b3dbd521984a1b450329264f2f3f9095b223 Mon Sep 17 00:00:00 2001 From: Jacob Moura Date: Fri, 10 Dec 2021 16:49:23 -0300 Subject: [PATCH 7/9] change documentation --- .../flutter_modular/dependency-injection.md | 2 +- doc/docs/flutter_modular/module.md | 2 +- doc/docs/flutter_modular/test.md | 2 +- .../flutter_modular/triple-integration.md | 2 +- doc/docs/flutter_modular/watch.md | 72 +++++++++++++++++++ doc/docs/flutter_modular/widgets.md | 2 +- doc/package-lock.json | 2 + 7 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 doc/docs/flutter_modular/watch.md diff --git a/doc/docs/flutter_modular/dependency-injection.md b/doc/docs/flutter_modular/dependency-injection.md index 3461bd04..dbb2a1ea 100644 --- a/doc/docs/flutter_modular/dependency-injection.md +++ b/doc/docs/flutter_modular/dependency-injection.md @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 4 --- # Dependency Injection diff --git a/doc/docs/flutter_modular/module.md b/doc/docs/flutter_modular/module.md index 2fc04934..717bdd49 100644 --- a/doc/docs/flutter_modular/module.md +++ b/doc/docs/flutter_modular/module.md @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 5 --- # Module diff --git a/doc/docs/flutter_modular/test.md b/doc/docs/flutter_modular/test.md index 3f11974e..1d5c0309 100644 --- a/doc/docs/flutter_modular/test.md +++ b/doc/docs/flutter_modular/test.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 7 --- # Tests diff --git a/doc/docs/flutter_modular/triple-integration.md b/doc/docs/flutter_modular/triple-integration.md index 2271d97f..974ddf3d 100644 --- a/doc/docs/flutter_modular/triple-integration.md +++ b/doc/docs/flutter_modular/triple-integration.md @@ -1,5 +1,5 @@ --- -sidebar_position: 7 +sidebar_position: 8 --- # Triple Pattern integration diff --git a/doc/docs/flutter_modular/watch.md b/doc/docs/flutter_modular/watch.md new file mode 100644 index 00000000..ce70f9d0 --- /dev/null +++ b/doc/docs/flutter_modular/watch.md @@ -0,0 +1,72 @@ +--- +sidebar_position: 3 +--- + +# Watch + +Modular also has an InheritedWidget instance that can use the `dependsOn` API of [BuildContext]. +This way, the developer can recover binds and, if it is a supported reactivity, will watch the modifications +changing the state of the widget in question. + +The `watch()` method was added to [BuildContext] through extensions, making access easy. +```dart +class Body extends StatelessWidget { + Widget build(BuildContext context){ + final notifier = context.watch(); + return Text('${notifier.value}') + } +} +``` + +Supported reactivities are: +- [Listenable](https://api.flutter.dev/flutter/foundation/Listenable-class.html) + +Used in `ChangeNotifier/ValueNotifier` classes and in RxNotifier. + +- [Stream](https://api.dart.dev/stable/2.15.0/dart-async/Stream-class.html) + +Used in StreamController or BLoC/Cubit + +- [Store](https://triple.flutterando.com.br/docs/getting-started/using-flutter-triple) + +Used in Triple Pattern in StreamStore and NotifierStore classes. + +:::tip TIP + +In addition to the **context.watch()** method, the read-only **context.read()** method has been added. +It's the same as using **Modular.get()**, but this addition helps projects that are being migrated +**Provider**. + +::: + +## With selectors + +Sometimes binds are not a supported reactivity, but one of their properties can be. +As in the case of BLoC, where the Stream is available through a `bloc.stream` property; + +We can add a selection through an anonymous function indicating which property is a supported reactivity to be watched: + +```dart +class Body extends StatelessWidget { + Widget build(BuildContext context){ + final bloc = context.watch((bloc) => bloc.stream); + return Text('${bloc.state}') + } +} +``` + +Note that the use of the selector does not change on bind return. + +We can also use selectors for Triple objects, which have their own selectors for each of their segments: +See the Triple documentation for more details [by clicking here](https://triple.flutterando.com.br/docs/getting-started/using-flutter-triple#selectors): + +```dart +class OnlyErrorWidget extends StatelessWidget { + Widget build(BuildContext context){ + // changes with store.setError(); + final store = context.watch((store) => store.selectError); + return Text('${store.error}') + } +} +``` + diff --git a/doc/docs/flutter_modular/widgets.md b/doc/docs/flutter_modular/widgets.md index e177a8c9..0bfad906 100644 --- a/doc/docs/flutter_modular/widgets.md +++ b/doc/docs/flutter_modular/widgets.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 6 --- # Widgets diff --git a/doc/package-lock.json b/doc/package-lock.json index 5a60e4e4..ac4b478d 100644 --- a/doc/package-lock.json +++ b/doc/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "doc", "version": "0.0.0", "dependencies": { "@docusaurus/core": "2.0.0-beta.6", @@ -23938,6 +23939,7 @@ "chalk": "^4.1.0", "find-up": "^5.0.0", "mkdirp": "^1.0.4", + "postcss": "^8.2.4", "strip-json-comments": "^3.1.1" }, "dependencies": { From 3f7f32cc8e050379e4134ce198d7821c750a2ec6 Mon Sep 17 00:00:00 2001 From: Jacob Moura Date: Fri, 10 Dec 2021 16:50:05 -0300 Subject: [PATCH 8/9] added link --- flutter_modular/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter_modular/CHANGELOG.md b/flutter_modular/CHANGELOG.md index 365b47f6..7074e5ed 100644 --- a/flutter_modular/CHANGELOG.md +++ b/flutter_modular/CHANGELOG.md @@ -30,6 +30,8 @@ class OnlyErrorWidget extends StatelessWidget { } ``` +See more details (here)[https://modular.flutterando.com.br/docs/flutter_modular/watch] + ## [4.2.0] - 2021-10-28 * Added cleanInjector() and cleanModular() for restart Modular. [#601](https://github.com/Flutterando/modular/pull/601) * Updated modular_core. From a6b85cb1b58e8b024bead6500a005a31553c4d3a Mon Sep 17 00:00:00 2001 From: Jacob Moura Date: Fri, 10 Dec 2021 16:50:37 -0300 Subject: [PATCH 9/9] fix link --- flutter_modular/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_modular/CHANGELOG.md b/flutter_modular/CHANGELOG.md index 7074e5ed..746db12b 100644 --- a/flutter_modular/CHANGELOG.md +++ b/flutter_modular/CHANGELOG.md @@ -30,7 +30,7 @@ class OnlyErrorWidget extends StatelessWidget { } ``` -See more details (here)[https://modular.flutterando.com.br/docs/flutter_modular/watch] +See more details [here](https://modular.flutterando.com.br/docs/flutter_modular/watch) ## [4.2.0] - 2021-10-28 * Added cleanInjector() and cleanModular() for restart Modular. [#601](https://github.com/Flutterando/modular/pull/601)