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": { diff --git a/flutter_modular/CHANGELOG.md b/flutter_modular/CHANGELOG.md index 9faaef0e..746db12b 100644 --- a/flutter_modular/CHANGELOG.md +++ b/flutter_modular/CHANGELOG.md @@ -1,3 +1,37 @@ +## [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}') + } +} +``` +* 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}') + } +} +``` + +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}') + } +} +``` + +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. diff --git a/flutter_modular/lib/src/presenter/widgets/modular_app.dart b/flutter_modular/lib/src/presenter/widgets/modular_app.dart index 0b5d223f..5e8c57ff 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,128 @@ 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); + final inherited = context.dependOnInheritedWidgetOfExactType<_ModularInherited>(aspect: registre)!; + inherited.updateShouldNotify(inherited); + } + + 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) { + var registers = getDependencies(dependent) as Set<_Register>?; + + registers ??= {}; + + if (registers.contains(aspect)) { + 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), + ); + } + registers.add(aspect); + setDependencies(dependent, registers); + } + + @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) { + var registers = getDependencies(dependent) as Set<_Register>?; + registers ??= {}; + + for (var register in registers) { + if (register.type == current) { + dependent.didChangeDependencies(); + } + } + } +} + +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); } } 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: 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(); + } +}