From 6f13b2c5c357f4312e9d6e0b703e49c47b48194f Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Wed, 27 Dec 2023 00:23:46 +0400 Subject: [PATCH] Add InheritedOctopus --- example/lib/src/common/router/routes.dart | 4 +- .../lib/src/common/widget/history_button.dart | 6 +-- .../account/widget/profile_screen.dart | 6 +-- .../authentication/widget/signin_screen.dart | 4 +- .../gallery/widget/gallery_screen.dart | 2 +- .../src/feature/home/widget/home_screen.dart | 4 +- .../feature/shop/widget/basket_screen.dart | 2 +- .../shop/widget/catalog_breadcrumbs.dart | 4 +- .../feature/shop/widget/catalog_screen.dart | 4 +- .../feature/shop/widget/category_screen.dart | 4 +- .../feature/shop/widget/checkout_screen.dart | 2 +- .../feature/shop/widget/favorites_screen.dart | 2 +- .../src/feature/shop/widget/shop_screen.dart | 6 +-- lib/octopus.dart | 1 + lib/src/controller/delegate.dart | 53 ++----------------- lib/src/controller/octopus.dart | 14 +++-- lib/src/state/state.dart | 9 ++-- lib/src/widget/bucket_navigator.dart | 7 +-- lib/src/widget/build_context_extension.dart | 19 +++++++ lib/src/widget/inherited_octopus.dart | 50 +++++++++++++++++ lib/src/widget/navigator.dart | 21 -------- 21 files changed, 119 insertions(+), 105 deletions(-) create mode 100644 lib/src/widget/build_context_extension.dart create mode 100644 lib/src/widget/inherited_octopus.dart diff --git a/example/lib/src/common/router/routes.dart b/example/lib/src/common/router/routes.dart index 9741ee4..215aa61 100644 --- a/example/lib/src/common/router/routes.dart +++ b/example/lib/src/common/router/routes.dart @@ -77,7 +77,7 @@ enum Routes with OctopusRoute { /// Pushes the [route] to the catalog tab. /// [id] is the product or category id for the [route]. static void pushToCatalog(BuildContext context, Routes route, String id) => - Octopus.of(context).setState((state) { + context.octopus.setState((state) { final node = state.find((n) => n.name == 'catalog-tab'); if (node == null) { return state @@ -101,7 +101,7 @@ enum Routes with OctopusRoute { /// Pops the last [route] from the catalog tab. static void popFromCatalog(BuildContext context) => - Octopus.of(context).setState((state) { + context.octopus.setState((state) { final node = state.find((n) => n.name == 'catalog-tab'); if (node == null || node.children.length < 2) { return state diff --git a/example/lib/src/common/widget/history_button.dart b/example/lib/src/common/widget/history_button.dart index 5b1fa84..c1e04ec 100644 --- a/example/lib/src/common/widget/history_button.dart +++ b/example/lib/src/common/widget/history_button.dart @@ -88,9 +88,9 @@ class _HistorySearchWidgetState extends State<_HistorySearchWidget> { @override void initState() { super.initState(); - final octopus = Octopus.of(context); + final octopus = context.octopus; final routes = octopus.config.routerDelegate.routes; - _observer = octopus.stateObserver; + _observer = octopus.observer; _entries = _observer.history.reversed.skip(1).map((e) { final route = routes[e.state.children.lastOrNull?.name]; return (route?.title, e); @@ -108,7 +108,7 @@ class _HistorySearchWidgetState extends State<_HistorySearchWidget> { } void _select(OctopusHistoryEntry entry) { - final router = Octopus.of(context); + final router = context.octopus; _pop(); Future.delayed(const Duration(milliseconds: 250), () => router.setState((_) => entry.state)); diff --git a/example/lib/src/feature/account/widget/profile_screen.dart b/example/lib/src/feature/account/widget/profile_screen.dart index e9f709c..d4fa522 100644 --- a/example/lib/src/feature/account/widget/profile_screen.dart +++ b/example/lib/src/feature/account/widget/profile_screen.dart @@ -129,8 +129,7 @@ class ProfileScreen extends StatelessWidget { height: 1, ), ), - onTap: () => - Octopus.of(context).push(Routes.settingsDialog), + onTap: () => context.octopus.push(Routes.settingsDialog), ), ), const SizedBox(width: 16), @@ -160,8 +159,7 @@ class ProfileScreen extends StatelessWidget { height: 1, ), ), - onTap: () => - Octopus.of(context).push(Routes.aboutAppDialog), + onTap: () => context.octopus.push(Routes.aboutAppDialog), ), ), const SizedBox(height: 24), diff --git a/example/lib/src/feature/authentication/widget/signin_screen.dart b/example/lib/src/feature/authentication/widget/signin_screen.dart index f3a7a9e..43d61bb 100644 --- a/example/lib/src/feature/authentication/widget/signin_screen.dart +++ b/example/lib/src/feature/authentication/widget/signin_screen.dart @@ -328,8 +328,8 @@ mixin _UsernamePasswordFormStateMixin on State { void signUp(BuildContext context) { FocusScope.of(context).unfocus(); // url_launcher.launchUrlString('...').ignore(); - // Octopus.of(context).setState((state) => state..add(Routes.signup.node())); - Octopus.of(context).push(Routes.signup); + // context.octopus.setState((state) => state..add(Routes.signup.node())); + context.octopus.push(Routes.signup); } @override diff --git a/example/lib/src/feature/gallery/widget/gallery_screen.dart b/example/lib/src/feature/gallery/widget/gallery_screen.dart index c78c9f9..8c929d6 100644 --- a/example/lib/src/feature/gallery/widget/gallery_screen.dart +++ b/example/lib/src/feature/gallery/widget/gallery_screen.dart @@ -61,7 +61,7 @@ class _GalleryTile extends StatelessWidget { builder: (context) => const GalleryDetailScreen(), ), ); */ - Octopus.of(context).push( + context.octopus.push( Routes.picture, arguments: {'id': id}, ); diff --git a/example/lib/src/feature/home/widget/home_screen.dart b/example/lib/src/feature/home/widget/home_screen.dart index 47c85f9..1d219c0 100644 --- a/example/lib/src/feature/home/widget/home_screen.dart +++ b/example/lib/src/feature/home/widget/home_screen.dart @@ -24,12 +24,12 @@ class HomeScreen extends StatelessWidget { ListTile( title: const Text('Shop'), subtitle: const Text('Explore nested navigation'), - onTap: () => Octopus.of(context).push(Routes.shop), + onTap: () => context.octopus.push(Routes.shop), ), ListTile( title: const Text('Gallery'), subtitle: const Text('Gallery description'), - onTap: () => Octopus.of(context).push(Routes.gallery), + onTap: () => context.octopus.push(Routes.gallery), ), ], ), diff --git a/example/lib/src/feature/shop/widget/basket_screen.dart b/example/lib/src/feature/shop/widget/basket_screen.dart index 099542d..857a0a1 100644 --- a/example/lib/src/feature/shop/widget/basket_screen.dart +++ b/example/lib/src/feature/shop/widget/basket_screen.dart @@ -99,7 +99,7 @@ class BasketScreen extends StatelessWidget { width: double.infinity, height: 48, child: ElevatedButton.icon( - onPressed: () => Octopus.of(context).setState((state) => + onPressed: () => context.octopus.setState((state) => state ..findByName('basket-tab') ?.add(Routes.checkout.node())), diff --git a/example/lib/src/feature/shop/widget/catalog_breadcrumbs.dart b/example/lib/src/feature/shop/widget/catalog_breadcrumbs.dart index b4153e7..c788178 100644 --- a/example/lib/src/feature/shop/widget/catalog_breadcrumbs.dart +++ b/example/lib/src/feature/shop/widget/catalog_breadcrumbs.dart @@ -68,8 +68,8 @@ class _CatalogBreadcrumbsState extends State { @override void initState() { super.initState(); - _router = Octopus.of(context); - _stateObserver = _router.stateObserver; + _router = context.octopus; + _stateObserver = _router.observer; if (widget.rebuilds) _stateObserver.addListener(_onStateChange); _onStateChange(rebuild: true); } diff --git a/example/lib/src/feature/shop/widget/catalog_screen.dart b/example/lib/src/feature/shop/widget/catalog_screen.dart index be41c7f..70afcde 100644 --- a/example/lib/src/feature/shop/widget/catalog_screen.dart +++ b/example/lib/src/feature/shop/widget/catalog_screen.dart @@ -187,7 +187,7 @@ class _CatalogTile extends StatelessWidget { Routes.category, category.id, ), - /* onTap: () => Octopus.of(context).setState( + /* onTap: () => context.octopus.setState( (state) => state ..add(Routes.category.node( arguments: {'id': category.id}, @@ -215,7 +215,7 @@ class _RecentlyViewedProductsState extends State<_RecentlyViewedProducts> { @override void initState() { super.initState(); - observer = Octopus.of(context).stateObserver; + observer = context.octopus.observer; observer.addListener(_onOctopusStateChanged); _onOctopusStateChanged(); } diff --git a/example/lib/src/feature/shop/widget/category_screen.dart b/example/lib/src/feature/shop/widget/category_screen.dart index e857583..d5c56e7 100644 --- a/example/lib/src/feature/shop/widget/category_screen.dart +++ b/example/lib/src/feature/shop/widget/category_screen.dart @@ -106,7 +106,7 @@ class CategoriesSliverListView extends StatelessWidget { return ListTile( key: ValueKey(category.id), title: Text(category.title), - onTap: () => Octopus.of(context).setState((state) => state + onTap: () => context.octopus.setState((state) => state ..findByName('catalog-tab')?.add(Routes.category.node( arguments: {'id': category.id}, ))), @@ -246,7 +246,7 @@ class _ProductTile extends StatelessWidget { splashColor: theme.splashColor, highlightColor: theme.highlightColor, onTap: () => onTap == null - ? Octopus.of(context).setState((state) => state + ? context.octopus.setState((state) => state ..findByName('catalog-tab')?.add(Routes.product.node( arguments: { 'id': product.id.toString() diff --git a/example/lib/src/feature/shop/widget/checkout_screen.dart b/example/lib/src/feature/shop/widget/checkout_screen.dart index 3f7e695..9fadca3 100644 --- a/example/lib/src/feature/shop/widget/checkout_screen.dart +++ b/example/lib/src/feature/shop/widget/checkout_screen.dart @@ -16,7 +16,7 @@ class CheckoutScreen extends StatelessWidget { const CheckoutScreen({super.key}); void pay(BuildContext context) { - Octopus.of(context).setState((state) => state + context.octopus.setState((state) => state ..removeByName(Routes.checkout.name) ..arguments['shop'] = ShopTabsEnum.catalog.name); ScaffoldMessenger.maybeOf(context)?.showSnackBar( diff --git a/example/lib/src/feature/shop/widget/favorites_screen.dart b/example/lib/src/feature/shop/widget/favorites_screen.dart index 72ccfec..fba38f9 100644 --- a/example/lib/src/feature/shop/widget/favorites_screen.dart +++ b/example/lib/src/feature/shop/widget/favorites_screen.dart @@ -69,7 +69,7 @@ class FavoritesScreen extends StatelessWidget { sliver: ProductsSliverGridView( products: products, onTap: (context, product) { - Octopus.of(context).setState((state) { + context.octopus.setState((state) { final node = state.find((n) => n.name == 'catalog-tab'); if (node == null) { return state diff --git a/example/lib/src/feature/shop/widget/shop_screen.dart b/example/lib/src/feature/shop/widget/shop_screen.dart index bfae533..1ecf912 100644 --- a/example/lib/src/feature/shop/widget/shop_screen.dart +++ b/example/lib/src/feature/shop/widget/shop_screen.dart @@ -98,7 +98,7 @@ class _ShopScreenState extends State { @override void initState() { super.initState(); - _octopusStateObserver = Octopus.of(context).stateObserver; + _octopusStateObserver = context.octopus.observer; // Restore tab from router arguments _tab = ShopTabsEnum.fromValue( @@ -118,13 +118,13 @@ class _ShopScreenState extends State { void _switchTab(ShopTabsEnum tab) { if (!mounted) return; if (_tab == tab) return; - Octopus.of(context).setArguments((args) => args['shop'] = tab.name); + context.octopus.setArguments((args) => args['shop'] = tab.name); setState(() => _tab = tab); } // Pop to catalog at double tap on catalog tab void _clearCatalogNavigationStack() { - Octopus.of(context).setState((state) { + context.octopus.setState((state) { final catalog = state.findByName('catalog-tab'); if (catalog == null || catalog.children.length < 2) return state; catalog.children.length = 1; diff --git a/lib/octopus.dart b/lib/octopus.dart index f3bfd31..dc9a8bf 100644 --- a/lib/octopus.dart +++ b/lib/octopus.dart @@ -6,6 +6,7 @@ export 'src/controller/guard.dart'; export 'src/controller/octopus.dart'; export 'src/state/state.dart'; export 'src/widget/bucket_navigator.dart'; +export 'src/widget/build_context_extension.dart'; export 'src/widget/navigator.dart'; export 'src/widget/no_animation.dart'; export 'src/widget/route_context.dart'; diff --git a/lib/src/controller/delegate.dart b/lib/src/controller/delegate.dart index a9b7012..a5a16cd 100644 --- a/lib/src/controller/delegate.dart +++ b/lib/src/controller/delegate.dart @@ -13,6 +13,7 @@ import 'package:octopus/src/state/node_extra_storage.dart'; import 'package:octopus/src/state/state.dart'; import 'package:octopus/src/util/logs.dart'; import 'package:octopus/src/util/state_util.dart'; +import 'package:octopus/src/widget/inherited_octopus.dart'; import 'package:octopus/src/widget/navigator.dart'; import 'package:octopus/src/widget/no_animation.dart'; @@ -123,7 +124,9 @@ final class OctopusDelegate extends RouterDelegate } @override - Widget build(BuildContext context) => _Stf( + Widget build(BuildContext context) => InheritedOctopus( + octopus: $controller.target!, + state: _observer.value, child: OctopusNavigator( router: $controller.target!, restorationScopeId: _restorationScopeId, @@ -567,51 +570,3 @@ final class OctopusHistoryEntry implements Comparable { timestamp == other.timestamp && state == other.state; } - -class _Stf extends StatefulWidget { - const _Stf({ - required this.child, - super.key, // ignore: unused_element - }); - - /// The widget below this widget in the tree. - final Widget child; - - @override - State<_Stf> createState() => __StfState(); -} - -/// State for widget _Stf. -class __StfState extends State<_Stf> with _StfController { - /* #region Lifecycle */ - @override - void initState() { - super.initState(); - } - - @override - void didUpdateWidget(_Stf oldWidget) { - super.didUpdateWidget(oldWidget); - // Widget configuration changed - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - // The configuration of InheritedWidgets has changed - // Also called after initState but before build - } - - @override - void dispose() { - // Permanent removal of a tree stent - super.dispose(); - } - /* #endregion */ - - @override - Widget build(BuildContext context) => widget.child; -} - -/// Controller for widget _Stf -mixin _StfController {} diff --git a/lib/src/controller/octopus.dart b/lib/src/controller/octopus.dart index e015d5c..5477c60 100644 --- a/lib/src/controller/octopus.dart +++ b/lib/src/controller/octopus.dart @@ -11,7 +11,7 @@ import 'package:octopus/src/controller/information_parser.dart'; import 'package:octopus/src/controller/information_provider.dart'; import 'package:octopus/src/state/state.dart'; import 'package:octopus/src/util/state_util.dart'; -import 'package:octopus/src/widget/navigator.dart'; +import 'package:octopus/src/widget/inherited_octopus.dart'; /// {@template octopus} /// The main class of the package. @@ -39,10 +39,18 @@ abstract base class Octopus { /// Receives the [Octopus] instance from the elements tree. static Octopus? maybeOf(BuildContext context) => - OctopusNavigator.maybeOf(context); + InheritedOctopus.maybeOf(context, listen: false)?.octopus; /// Receives the [Octopus] instance from the elements tree. - static Octopus of(BuildContext context) => OctopusNavigator.of(context); + static Octopus of(BuildContext context) => + InheritedOctopus.of(context, listen: false).octopus; + + /// Receives the current [OctopusState] instance from the elements tree. + static OctopusState$Immutable stateOf( + BuildContext context, { + bool listen = true, + }) => + InheritedOctopus.of(context, listen: true).state; /// Receives the last initializated [Octopus] instance. static Octopus get instance => diff --git a/lib/src/state/state.dart b/lib/src/state/state.dart index f956a9a..f4dceea 100644 --- a/lib/src/state/state.dart +++ b/lib/src/state/state.dart @@ -730,7 +730,7 @@ mixin OctopusRoute { _defaultPageBuilder = fn; static DefaultOctopusPageBuilder _defaultPageBuilder = (context, route, node) => MaterialPage( - key: ValueKey(node.key), + key: route.createKey(node), child: InheritedOctopusRoute( node: node, child: route.builder(context, node), @@ -762,6 +762,9 @@ mixin OctopusRoute { /// ``` Widget builder(BuildContext context, OctopusNode node); + /// Create [LocalKey] for [Page] of this route using [OctopusNode]. + LocalKey createKey(OctopusNode node) => ValueKey(node.key); + /// Build [Page] for this route using [BuildContext] and [OctopusNode]. /// [BuildContext] - Navigator context. /// [OctopusNode] - Current node of the router state tree. @@ -771,14 +774,14 @@ mixin OctopusRoute { Page pageBuilder(BuildContext context, OctopusNode node) => node.name.endsWith('-dialog') ? OctopusDialogPage( - key: ValueKey(node.key), + key: createKey(node), builder: (context) => builder(context, node), name: node.name, arguments: node.arguments, ) : NoAnimationScope.of(context) ? NoAnimationPage( - key: ValueKey(node.key), + key: createKey(node), child: InheritedOctopusRoute( node: node, child: builder(context, node), diff --git a/lib/src/widget/bucket_navigator.dart b/lib/src/widget/bucket_navigator.dart index 08e551d..2204479 100644 --- a/lib/src/widget/bucket_navigator.dart +++ b/lib/src/widget/bucket_navigator.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:octopus/src/controller/delegate.dart'; import 'package:octopus/src/controller/octopus.dart'; import 'package:octopus/src/state/state.dart'; +import 'package:octopus/src/widget/build_context_extension.dart'; import 'package:octopus/src/widget/navigator.dart'; import 'package:octopus/src/widget/no_animation.dart'; import 'package:octopus/src/widget/route_context.dart'; @@ -63,8 +64,8 @@ class _BucketNavigatorState extends State @override void initState() { super.initState(); - _router = Octopus.of(context); - _observer = _router.stateObserver; + _router = context.octopus; + _observer = _router.observer; _observer.addListener(_handleStateChange); _handleStateChange(); } @@ -187,7 +188,7 @@ mixin _BackButtonBucketNavigatorStateMixin on State { void initState() { // TODO(plugfox): check priority for nested navigators dispatcher?.removeCallback(_onBackButtonPressed); - final rootBackDispatcher = Octopus.of(context).config.backButtonDispatcher; + final rootBackDispatcher = context.octopus.config.backButtonDispatcher; dispatcher = rootBackDispatcher.createChildBackButtonDispatcher() ..addCallback(_onBackButtonPressed) ..takePriority(); diff --git a/lib/src/widget/build_context_extension.dart b/lib/src/widget/build_context_extension.dart new file mode 100644 index 0000000..810c2dc --- /dev/null +++ b/lib/src/widget/build_context_extension.dart @@ -0,0 +1,19 @@ +import 'package:flutter/widgets.dart' show BuildContext; +import 'package:octopus/src/controller/octopus.dart'; +import 'package:octopus/src/state/state.dart'; +import 'package:octopus/src/widget/inherited_octopus.dart'; + +/// Extension methods for [BuildContext]. +extension OctopusBuildContextExtension on BuildContext { + /// Receives the [Octopus] instance from the elements tree. + Octopus get octopus => InheritedOctopus.of(this, listen: false).octopus; + + /// Receives the current [OctopusState] instance from the elements tree. + OctopusState$Immutable get readOctopusState => + InheritedOctopus.of(this, listen: false).state; + + /// Receives the current [OctopusState] instance from the elements tree + /// and listen for changes. + OctopusState$Immutable get watchOctopusState => + InheritedOctopus.of(this, listen: true).state; +} diff --git a/lib/src/widget/inherited_octopus.dart b/lib/src/widget/inherited_octopus.dart new file mode 100644 index 0000000..8d29bc7 --- /dev/null +++ b/lib/src/widget/inherited_octopus.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:octopus/src/controller/octopus.dart'; +import 'package:octopus/src/state/state.dart'; + +/// InheritedOctopus widget. +/// {@nodoc} +class InheritedOctopus extends InheritedWidget { + /// {@nodoc} + const InheritedOctopus({ + required super.child, + required this.octopus, + required this.state, + super.key, // ignore: unused_element + }); + + /// Receives the [Octopus] instance from the elements tree. + /// {@nodoc} + final Octopus octopus; + + /// Receives the [OctopusState] instance from the elements tree. + /// {@nodoc} + final OctopusState$Immutable state; + + /// The state from the closest instance of this class + /// that encloses the given context, if any. + /// e.g. `InheritedOctopus.maybeOf(context)`. + /// {@nodoc} + static InheritedOctopus? maybeOf(BuildContext context, + {bool listen = true}) => + listen + ? context.dependOnInheritedWidgetOfExactType() + : context.getInheritedWidgetOfExactType(); + + static Never _notFoundInheritedWidgetOfExactType() => throw ArgumentError( + 'Out of scope, not found inherited widget ' + 'a InheritedOctopus of the exact type', + 'out_of_scope', + ); + + /// The state from the closest instance of this class + /// that encloses the given context. + /// e.g. `InheritedOctopus.of(context)` + /// {@nodoc} + static InheritedOctopus of(BuildContext context, {bool listen = true}) => + maybeOf(context, listen: listen) ?? _notFoundInheritedWidgetOfExactType(); + + @override + bool updateShouldNotify(covariant InheritedOctopus oldWidget) => + state != oldWidget.state; +} diff --git a/lib/src/widget/navigator.dart b/lib/src/widget/navigator.dart index 622e13b..c02f844 100644 --- a/lib/src/widget/navigator.dart +++ b/lib/src/widget/navigator.dart @@ -23,27 +23,6 @@ class OctopusNavigator extends Navigator { super.key, }) : _router = router; - /// Receives the [Octopus] instance from the elements tree. - static Octopus? maybeOf(BuildContext context) { - Octopus? controller; - context.visitAncestorElements((element) { - if (element is _OctopusNavigatorContext) { - controller = element.router; - if (controller != null) return false; - } - return true; - }); - return controller; - } - - static Never _notFound() => throw ArgumentError( - 'Out of scope, not found a OctopusNavigator widget', - 'out_of_scope', - ); - - /// Receives the [Octopus] instance from the elements tree. - static Octopus of(BuildContext context) => maybeOf(context) ?? _notFound(); - /// {@nodoc} final Octopus _router;