Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notifiers feature #679

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion package/lib/src/beam_page.dart
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -36,7 +37,7 @@ enum BeamPageType {
}

/// A wrapper for screens in a navigation stack.
class BeamPage extends Page {
class BeamPage<T extends BeamPageInfo> extends Page {
/// Creates a [BeamPage] with specified properties.
///
/// [child] is required and typically represents a screen of the app.
Expand All @@ -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].
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand All @@ -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<BeamPageState>
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<T extends BeamPageInfo> extends ValueListenable<T?>
with ChangeNotifier {
BeamPageInfoNotifier();

@override
late T? value;
}
51 changes: 32 additions & 19 deletions package/lib/src/beam_stack.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends RouteInformationSerializable>
extends ChangeNotifier {
abstract class BeamStack<T extends RouteInformationSerializable,
K extends BeamPageInfo> extends ChangeNotifier {
/// Creates a [BeamStack] with specified properties.
///
/// All attributes can be null.
Expand Down Expand Up @@ -261,7 +261,7 @@ abstract class BeamStack<T extends RouteInformationSerializable>
}

/// The history of beaming for this.
List<HistoryElement> history = [];
final List<HistoryElement> history = [];

/// Adds another [HistoryElement] to [history] list.
/// The history element is created from given [state] and [beamParameters].
Expand Down Expand Up @@ -367,7 +367,7 @@ abstract class BeamStack<T extends RouteInformationSerializable>
///
/// [context] can be useful while building the pages.
/// It will also contain anything injected via [builder].
List<BeamPage> buildPages(BuildContext context, T state);
List<BeamPage<K>> buildPages(BuildContext context, T state);

/// Guards that will be executing [BeamGuard.check] when this gets beamed to.
///
Expand All @@ -391,7 +391,7 @@ abstract class BeamStack<T extends RouteInformationSerializable>
}

/// Default stack to choose if requested URI doesn't parse to any stack.
class NotFound extends BeamStack<BeamState> {
class NotFound extends BeamStack<BeamState, BeamPageInfo> {
/// Creates a [NotFound] [BeamStack] with
/// `RouteInformation(uri: Uri.parse(path)` as its state.
NotFound({String path = '/'}) : super(RouteInformation(uri: Uri.parse(path)));
Expand All @@ -406,7 +406,7 @@ class NotFound extends BeamStack<BeamState> {
/// Empty stack used to initialize a non-nullable BeamStack variable.
///
/// See [BeamerDelegate.currentBeamStack].
class EmptyBeamStack extends BeamStack<BeamState> {
class EmptyBeamStack extends BeamStack<BeamState, BeamPageInfo> {
@override
List<BeamPage> buildPages(BuildContext context, BeamState state) => [];

Expand All @@ -415,7 +415,7 @@ class EmptyBeamStack extends BeamStack<BeamState> {
}

/// A specific single-page [BeamStack] for [BeamGuard.showPage]
class GuardShowPage extends BeamStack<BeamState> {
class GuardShowPage extends BeamStack<BeamState, BeamPageInfo> {
/// Creates a [GuardShowPage] [BeamStack] with
/// `RouteInformation(uri: Uri.parse(path)` as its state.
GuardShowPage(
Expand All @@ -440,23 +440,27 @@ class GuardShowPage extends BeamStack<BeamState> {
/// 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<BeamState> {
class RoutesBeamStack<T extends BeamPageInfo> extends BeamStack<BeamState, T> {
/// Creates a [RoutesBeamStack] with specified properties.
///
/// [routeInformation] and [routes] are required.
RoutesBeamStack({
required this.parent,
required RouteInformation routeInformation,
Object? data,
BeamParameters? beamParameters,
required this.routes,
this.navBuilder,
}) : super(routeInformation, beamParameters);

final BeamerDelegate<T> parent;

/// Map of all routes this stack handles.
Map<Pattern, dynamic Function(BuildContext, BeamState, Object? data)> routes;
final Map<Pattern, dynamic Function(BuildContext, BeamState, Object? data)>
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) {
Expand All @@ -483,23 +487,32 @@ class RoutesBeamStack extends BeamStack<BeamState> {
List<Pattern> get pathPatterns => routes.keys.toList();

@override
List<BeamPage> buildPages(BuildContext context, BeamState state) {
List<BeamPage<T>> 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<BeamPage>((route) {
final pages = sortedRoutes.indexed.map<BeamPage<T>>((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<T> page = routeElement is BeamPage
? routeElement as BeamPage<T>
: BeamPage<T>(
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;
}

Expand Down
72 changes: 69 additions & 3 deletions package/lib/src/beamer_delegate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<RouteInformation>
class BeamerDelegate<T extends BeamPageInfo>
extends RouterDelegate<RouteInformation>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteInformation> {
/// 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,
Expand Down Expand Up @@ -50,6 +52,12 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>
updateListenable?.addListener(_update);
}

final String debugLabel;

final Map<LocalKey, BeamPageStateNotifier> pageStateNotifiers = {};

bool _firstBuild = true;

/// A state of this delegate. This is the `routeInformation` that goes into
/// [stackBuilder] to build an appropriate [BeamStack].
///
Expand Down Expand Up @@ -352,6 +360,8 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>
/// to avoid setting URL when the guards have not been run yet.
bool _initialConfigurationReady = false;

final pinnaclePageInfoNotifier = BeamPageInfoNotifier<T>();

/// Main method to update the [configuration] of this delegate and its
/// [currentBeamStack].
///
Expand Down Expand Up @@ -424,6 +434,7 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>
if (buildBeamStack) {
// build a BeamStack from configuration
_beamStackCandidate = stackBuilder(
this,
this.configuration.copyWith(),
_currentBeamParameters,
);
Expand Down Expand Up @@ -754,6 +765,9 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>

@override
Widget build(BuildContext context) {
final isFirstBuild = this._firstBuild;
_firstBuild = false;

_buildInProgress = true;
_context = context;

Expand All @@ -780,6 +794,13 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>
_setBrowserTitle(context);

buildListener?.call(context, this);

// Notifying pinnacle page info
pinnaclePageInfoNotifier.notifyListeners();

// Notifying pages states
_notifyCurrentPagesStates(isFirstBuild: isFirstBuild);

return Navigator(
key: navigatorKey,
observers: navigatorObservers,
Expand Down Expand Up @@ -969,6 +990,7 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>
_beamStackCandidate = notFoundRedirect!;
} else if (notFoundRedirectNamed != null) {
_beamStackCandidate = stackBuilder(
this,
RouteInformation(uri: Uri.parse(notFoundRedirectNamed!)),
_currentBeamParameters.copyWith(),
);
Expand All @@ -977,12 +999,52 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>
}

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);
}
}

Expand Down Expand Up @@ -1047,7 +1109,7 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>
final parentConfiguration = _parent!.configuration.copyWith();
if (initializeFromParent) {
_beamStackCandidate =
stackBuilder(parentConfiguration, _currentBeamParameters);
stackBuilder(this, parentConfiguration, _currentBeamParameters);
}

// If this couldn't handle parents configuration,
Expand Down Expand Up @@ -1077,7 +1139,11 @@ class BeamerDelegate extends RouterDelegate<RouteInformation>
// 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(
Expand Down
Loading
Loading