diff --git a/package/lib/src/beam_page.dart b/package/lib/src/beam_page.dart index 7199fb1..fdf91ca 100644 --- a/package/lib/src/beam_page.dart +++ b/package/lib/src/beam_page.dart @@ -1,6 +1,7 @@ import 'package:beamer/beamer.dart'; import 'package:beamer/src/utils.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// Types for how to route should be built. @@ -36,7 +37,7 @@ enum BeamPageType { } /// A wrapper for screens in a navigation stack. -class BeamPage extends Page { +class BeamPage extends Page { /// Creates a [BeamPage] with specified properties. /// /// [child] is required and typically represents a screen of the app. @@ -54,6 +55,8 @@ class BeamPage extends Page { this.fullScreenDialog = false, this.opaque = true, this.keepQueryOnPop = false, + this.stateChangeNotifier, + this.info, }) : super(key: key, name: name); /// A [BeamPage] to be the default for [BeamerDelegate.notFoundPage]. @@ -230,6 +233,12 @@ class BeamPage extends Page { /// Defaults to `false`. final bool keepQueryOnPop; + LocalKey get key => super.key!; + + final BeamPageStateNotifier? stateChangeNotifier; + + final T? info; + @override Route createRoute(BuildContext context) { if (routeBuilder != null) { @@ -323,6 +332,8 @@ class BeamPage extends Page { opaque: opaque, settings: this, pageBuilder: (context, animation, secondaryAnimation) => child, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, ); default: return MaterialPageRoute( @@ -333,3 +344,57 @@ class BeamPage extends Page { } } } + +/// Represents the [BeamPage] state inside a [BeamStack]. +/// +/// This is a volatile state, meaning it is not stored. On the contrary, it is +/// regenerated on [BeamerDelegate.build]. +/// +/// Initialy created to inform a page whether is the last (the pinnacle) +/// on the stack. +class BeamPageState { + BeamPageState({ + required this.isPinnacle, + }); + + final bool isPinnacle; + + @override + bool operator ==(Object other) => + other is BeamPageState && isPinnacle == other.isPinnacle; + + @override + int get hashCode => isPinnacle.hashCode; +} + +/// Utility to inform [BeamPageState] to his [BeamPage]. +class BeamPageStateNotifier extends ValueListenable + with ChangeNotifier { + BeamPageStateNotifier(); + + @override + late BeamPageState value; + + @override + void notifyListeners({bool ignore = false}) { + if (ignore) { + return; + } + + super.notifyListeners(); + } +} + +/// Represents specific page related information. +/// +/// Not represents state. +mixin BeamPageInfo {} + +/// Utility to inform the current [BeamPageInfo] of [BeamerDelegate]. +class BeamPageInfoNotifier extends ValueListenable + with ChangeNotifier { + BeamPageInfoNotifier(); + + @override + late T? value; +} diff --git a/package/lib/src/beam_stack.dart b/package/lib/src/beam_stack.dart index aa322f3..2999da3 100644 --- a/package/lib/src/beam_stack.dart +++ b/package/lib/src/beam_stack.dart @@ -83,8 +83,8 @@ class HistoryElement { /// * keeping a [state] that provides the link between the first 2 /// /// Extend this class to define your stacks to which you can then beam to. -abstract class BeamStack - extends ChangeNotifier { +abstract class BeamStack extends ChangeNotifier { /// Creates a [BeamStack] with specified properties. /// /// All attributes can be null. @@ -261,7 +261,7 @@ abstract class BeamStack } /// The history of beaming for this. - List history = []; + final List history = []; /// Adds another [HistoryElement] to [history] list. /// The history element is created from given [state] and [beamParameters]. @@ -367,7 +367,7 @@ abstract class BeamStack /// /// [context] can be useful while building the pages. /// It will also contain anything injected via [builder]. - List buildPages(BuildContext context, T state); + List> buildPages(BuildContext context, T state); /// Guards that will be executing [BeamGuard.check] when this gets beamed to. /// @@ -391,7 +391,7 @@ abstract class BeamStack } /// Default stack to choose if requested URI doesn't parse to any stack. -class NotFound extends BeamStack { +class NotFound extends BeamStack { /// Creates a [NotFound] [BeamStack] with /// `RouteInformation(uri: Uri.parse(path)` as its state. NotFound({String path = '/'}) : super(RouteInformation(uri: Uri.parse(path))); @@ -406,7 +406,7 @@ class NotFound extends BeamStack { /// Empty stack used to initialize a non-nullable BeamStack variable. /// /// See [BeamerDelegate.currentBeamStack]. -class EmptyBeamStack extends BeamStack { +class EmptyBeamStack extends BeamStack { @override List buildPages(BuildContext context, BeamState state) => []; @@ -415,7 +415,7 @@ class EmptyBeamStack extends BeamStack { } /// A specific single-page [BeamStack] for [BeamGuard.showPage] -class GuardShowPage extends BeamStack { +class GuardShowPage extends BeamStack { /// Creates a [GuardShowPage] [BeamStack] with /// `RouteInformation(uri: Uri.parse(path)` as its state. GuardShowPage( @@ -440,11 +440,12 @@ class GuardShowPage extends BeamStack { /// A beam stack for [RoutesStackBuilder], but can be used freely. /// /// Useful when needing a simple beam stack with a single or few pages. -class RoutesBeamStack extends BeamStack { +class RoutesBeamStack extends BeamStack { /// Creates a [RoutesBeamStack] with specified properties. /// /// [routeInformation] and [routes] are required. RoutesBeamStack({ + required this.parent, required RouteInformation routeInformation, Object? data, BeamParameters? beamParameters, @@ -452,11 +453,14 @@ class RoutesBeamStack extends BeamStack { this.navBuilder, }) : super(routeInformation, beamParameters); + final BeamerDelegate parent; + /// Map of all routes this stack handles. - Map routes; + final Map + routes; /// A wrapper used as [BeamStack.builder]. - Widget Function(BuildContext context, Widget navigator)? navBuilder; + final Widget Function(BuildContext context, Widget navigator)? navBuilder; @override Widget builder(BuildContext context, Widget navigator) { @@ -483,23 +487,32 @@ class RoutesBeamStack extends BeamStack { List get pathPatterns => routes.keys.toList(); @override - List buildPages(BuildContext context, BeamState state) { + List> buildPages(BuildContext context, BeamState state) { final filteredRoutes = chooseRoutes(state.routeInformation, routes.keys); final routeBuilders = Map.of(routes) ..removeWhere((key, value) => !filteredRoutes.containsKey(key)); final sortedRoutes = routeBuilders.keys.toList() ..sort((a, b) => _compareKeys(a, b)); - final pages = sortedRoutes.map((route) { + final pages = sortedRoutes.indexed.map>((value) { + // final index = value.$1; + final route = value.$2; final routeElement = routes[route]!(context, state, data); - if (routeElement is BeamPage) { - return routeElement; - } else { - return BeamPage( - key: ValueKey(filteredRoutes[route]), - child: routeElement, - ); + final BeamPage page = routeElement is BeamPage + ? routeElement as BeamPage + : BeamPage( + key: ValueKey(filteredRoutes[route]), + child: routeElement, + ); + + // Storing page state notifier + final stateChangeNotifier = page.stateChangeNotifier; + if (stateChangeNotifier != null) { + parent.pageStateNotifiers[page.key] = stateChangeNotifier; } + + return page; }).toList(); + return pages; } diff --git a/package/lib/src/beamer_delegate.dart b/package/lib/src/beamer_delegate.dart index c6a4140..9459785 100644 --- a/package/lib/src/beamer_delegate.dart +++ b/package/lib/src/beamer_delegate.dart @@ -10,12 +10,14 @@ import 'package:flutter/services.dart'; /// A delegate that is used by the [Router] to build the [Navigator]. /// /// This is "the beamer", the one that does the actual beaming. -class BeamerDelegate extends RouterDelegate +class BeamerDelegate + extends RouterDelegate with ChangeNotifier, PopNavigatorRouterDelegateMixin { /// Creates a [BeamerDelegate] with specified properties. /// /// [stackBuilder] is required to process the incoming navigation request. BeamerDelegate({ + required this.debugLabel, required this.stackBuilder, this.initialPath = '/', this.routeListener, @@ -50,6 +52,12 @@ class BeamerDelegate extends RouterDelegate updateListenable?.addListener(_update); } + final String debugLabel; + + final Map pageStateNotifiers = {}; + + bool _firstBuild = true; + /// A state of this delegate. This is the `routeInformation` that goes into /// [stackBuilder] to build an appropriate [BeamStack]. /// @@ -352,6 +360,8 @@ class BeamerDelegate extends RouterDelegate /// to avoid setting URL when the guards have not been run yet. bool _initialConfigurationReady = false; + final pinnaclePageInfoNotifier = BeamPageInfoNotifier(); + /// Main method to update the [configuration] of this delegate and its /// [currentBeamStack]. /// @@ -424,6 +434,7 @@ class BeamerDelegate extends RouterDelegate if (buildBeamStack) { // build a BeamStack from configuration _beamStackCandidate = stackBuilder( + this, this.configuration.copyWith(), _currentBeamParameters, ); @@ -754,6 +765,9 @@ class BeamerDelegate extends RouterDelegate @override Widget build(BuildContext context) { + final isFirstBuild = this._firstBuild; + _firstBuild = false; + _buildInProgress = true; _context = context; @@ -780,6 +794,13 @@ class BeamerDelegate extends RouterDelegate _setBrowserTitle(context); buildListener?.call(context, this); + + // Notifying pinnacle page info + pinnaclePageInfoNotifier.notifyListeners(); + + // Notifying pages states + _notifyCurrentPagesStates(isFirstBuild: isFirstBuild); + return Navigator( key: navigatorKey, observers: navigatorObservers, @@ -969,6 +990,7 @@ class BeamerDelegate extends RouterDelegate _beamStackCandidate = notFoundRedirect!; } else if (notFoundRedirectNamed != null) { _beamStackCandidate = stackBuilder( + this, RouteInformation(uri: Uri.parse(notFoundRedirectNamed!)), _currentBeamParameters.copyWith(), ); @@ -977,12 +999,52 @@ class BeamerDelegate extends RouterDelegate } void _setCurrentPages(BuildContext context) { + final currentBeamStack = this.currentBeamStack; + if (currentBeamStack is NotFound) { _currentPages = [notFoundPage]; + pageStateNotifiers.clear(); + pinnaclePageInfoNotifier.value = null; } else { _currentPages = _currentBeamParameters.stacked ? currentBeamStack.buildPages(context, currentBeamStack.state) : [currentBeamStack.buildPages(context, currentBeamStack.state).last]; + _purgePageStateNotifiers(); + pinnaclePageInfoNotifier.value = _currentPages.lastOrNull?.info as T?; + } + } + + /// Purges outdated page state notifiers. + void _purgePageStateNotifiers() { + final currentPagesKeys = _currentPages.map((page) => page.key); + pageStateNotifiers + .removeWhere((key, pageNotifier) => !currentPagesKeys.contains(key)); + } + + /// Notifies current pages [BeamPageState]'s. + void _notifyCurrentPagesStates({required bool isFirstBuild}) { + final stack = currentBeamStack; + + if (stack is! RoutesBeamStack) { + return; + } + + // Hidden pages + for (int i = 0; i < _currentPages.length - 1; i++) { + final notifier = pageStateNotifiers[_currentPages[i].key]; + if (notifier != null) { + notifier + ..value = BeamPageState(isPinnacle: false) + ..notifyListeners(ignore: isFirstBuild); + } + } + + // Pinnacle page + final notifier = pageStateNotifiers[_currentPages.last.key]; + if (notifier != null) { + notifier + ..value = BeamPageState(isPinnacle: true) + ..notifyListeners(ignore: isFirstBuild); } } @@ -1047,7 +1109,7 @@ class BeamerDelegate extends RouterDelegate final parentConfiguration = _parent!.configuration.copyWith(); if (initializeFromParent) { _beamStackCandidate = - stackBuilder(parentConfiguration, _currentBeamParameters); + stackBuilder(this, parentConfiguration, _currentBeamParameters); } // If this couldn't handle parents configuration, @@ -1077,7 +1139,11 @@ class BeamerDelegate extends RouterDelegate // Updates only if it can handle the configuration void _updateFromParent({bool rebuild = true}) { final parentConfiguration = _parent!.configuration.copyWith(); - final beamStack = stackBuilder(parentConfiguration, _currentBeamParameters); + final beamStack = stackBuilder( + this, + parentConfiguration, + _currentBeamParameters, + ); if (beamStack is! NotFound) { update( diff --git a/package/lib/src/stack_builders.dart b/package/lib/src/stack_builders.dart index 45fedc3..0f513f6 100644 --- a/package/lib/src/stack_builders.dart +++ b/package/lib/src/stack_builders.dart @@ -5,6 +5,7 @@ import 'package:beamer/src/utils.dart'; /// A convenience typedef for [BeamerDelegate.stackBuilder]. typedef StackBuilder = BeamStack Function( + BeamerDelegate parent, RouteInformation, BeamParameters?, ); @@ -52,18 +53,21 @@ class RoutesStackBuilder { final Map routes; /// Used as a [BeamStack.builder]. - Widget Function(BuildContext context, Widget navigator)? builder; + final Widget Function(BuildContext context, Widget navigator)? builder; /// Makes this callable as [StackBuilder]. /// - /// Returns [RoutesBeamStack] configured with chosen routes from [routes] or [NotFound]. + /// Returns [RoutesBeamStack] configured with chosen routes from [routes] + /// or [NotFound]. BeamStack call( + BeamerDelegate parent, RouteInformation routeInformation, BeamParameters? beamParameters, ) { final matched = RoutesBeamStack.chooseRoutes(routeInformation, routes.keys); if (matched.isNotEmpty) { return RoutesBeamStack( + parent: parent, routeInformation: routeInformation, routes: routes, navBuilder: builder,