Skip to content

Commit

Permalink
Add queue and guards
Browse files Browse the repository at this point in the history
  • Loading branch information
PlugFox committed Nov 27, 2023
1 parent f49f186 commit a402130
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 62 deletions.
1 change: 1 addition & 0 deletions lib/octopus.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
library octopus;

export 'src/controller/guard.dart';
export 'src/controller/octopus.dart';
export 'src/state/state.dart' show OctopusState, OctopusNode, OctopusRoute;
export 'src/widget/route_context.dart';
Expand Down
182 changes: 141 additions & 41 deletions lib/src/controller/delegate.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import 'dart:collection';
import 'dart:developer' as developer;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:octopus/src/controller/guard.dart';
import 'package:octopus/src/controller/octopus.dart';
import 'package:octopus/src/controller/state_queue.dart';
import 'package:octopus/src/state/state.dart';
import 'package:octopus/src/widget/octopus_navigator.dart';

Expand All @@ -17,22 +20,30 @@ final class OctopusDelegate extends RouterDelegate<OctopusState>
OctopusDelegate({
required OctopusState initialState,
required Map<String, OctopusRoute> routes,
required OctopusRoute defaultRoute,
List<OctopusState>? history,
List<IOctopusGuard>? guards,
String? restorationScopeId = 'octopus',
List<NavigatorObserver>? observers,
TransitionDelegate<Object?>? transitionDelegate,
RouteFactory? notFound,
void Function(Object error, StackTrace stackTrace)? onError,
}) : _stateObserver = _OctopusStateObserver(initialState),
}) : _stateObserver = _OctopusStateObserver(initialState, history),
_routes = routes,
_defaultRoute = defaultRoute,
_guards = guards?.toList(growable: false) ?? <IOctopusGuard>[],
_restorationScopeId = restorationScopeId,
_observers = observers,
_transitionDelegate =
transitionDelegate ?? const DefaultTransitionDelegate<Object?>(),
_notFound = notFound,
_onError = onError;
_onError = onError {
// Subscribe to the guards.
_guardsListener = Listenable.merge(_guards)..addListener(_onGuardsNotified);
}

final _OctopusStateObserver _stateObserver;
ValueListenable<OctopusState> get stateObserver => _stateObserver;
OctopusStateObserver get stateObserver => _stateObserver;

/// The restoration scope id for the navigator.
final String? _restorationScopeId;
Expand All @@ -56,6 +67,13 @@ final class OctopusDelegate extends RouterDelegate<OctopusState>
/// Routes hash table.
final Map<String, OctopusRoute> _routes;

/// Default fallback route.
final OctopusRoute _defaultRoute;

/// Guards.
final List<IOctopusGuard> _guards;
late final Listenable _guardsListener;

/// WidgetApp's navigator.
NavigatorState? get navigator => _modalObserver.navigator;
final NavigatorObserver _modalObserver = RouteObserver<ModalRoute<Object?>>();
Expand All @@ -73,10 +91,6 @@ final class OctopusDelegate extends RouterDelegate<OctopusState>
}
}

/// Current configuration.
@override
OctopusState get currentConfiguration => _stateObserver.value;

@override
Widget build(BuildContext context) => OctopusNavigator(
controller: $controller.target!,
Expand Down Expand Up @@ -107,20 +121,35 @@ final class OctopusDelegate extends RouterDelegate<OctopusState>
(_, __) => false,
);

@internal
List<Page<Object?>> buildPagesFromNodes(
BuildContext context, List<OctopusNode> nodes) =>
BuildContext context,
List<OctopusNode> nodes,
) =>
_handleErrors(() {
final pages = <Page<Object?>>[];
// Build pages
for (final node in nodes) {
final route = _routes[node.name];
assert(route != null, 'Route ${node.name} not found');
if (route == null) continue;
final page = route.pageBuilder(context, node);
pages.add(page);
try {
final route = _routes[node.name];
assert(route != null, 'Route ${node.name} not found');
if (route == null) continue;
final page = route.pageBuilder(context, node);
pages.add(page);
} on Object catch (error, stackTrace) {
developer.log(
'Failed to build page',
name: 'octopus',
error: error,
stackTrace: stackTrace,
level: 1000,
);
_onError?.call(error, stackTrace);
}
}
if (pages.isNotEmpty) return pages;
throw FlutterError('The Navigator.pages must not be empty to use the '
'Navigator.pages API');
// Build default page if no pages were built
return <Page<Object?>>[_defaultRoute.pageBuilder(context, nodes.first)];
}, (error, stackTrace) {
developer.log(
'Failed to build pages',
Expand Down Expand Up @@ -169,32 +198,32 @@ final class OctopusDelegate extends RouterDelegate<OctopusState>
() {
final route = _notFound?.call(settings);
if (route != null) return route;
/* _onError?.call(
OctopusUnknownRouteException(settings),
developer.log(
'Unknown route ${settings.name}',
name: 'octopus',
level: 1000,
stackTrace: StackTrace.current,
);
_onError?.call(
'Unknown route ${settings.name}',
StackTrace.current,
); */
);
return null;
},
(_, __) => null,
);

OctopusState? _$newConfigurationCache;
late final OctopusStateQueue _$stateChangeQueue =
OctopusStateQueue(processor: _setConfiguration);

@override
Future<void> setNewRoutePath(covariant OctopusState configuration) {
if (configuration.children.isEmpty) {
//assert(false, 'Configuration should not be empty');
return SynchronousFuture<void>(null);
}
_handleErrors(() {
OctopusState? newConfiguration = configuration;
// TODO(plugfox): validate and normalize configuration
_stateObserver.changeState(newConfiguration);
notifyListeners();
}, (_, __) {});

// Use [SynchronousFuture] so that the initial url is processed
// synchronously and remove unwanted initial animations on deep-linking
return SynchronousFuture<void>(null);
Future<void> setNewRoutePath(covariant OctopusState configuration) async {
// Normalize configuration
// ...
// Validate configuration
if (configuration.children.isEmpty) return;
// Add configuration to the queue
return _$stateChangeQueue.add(configuration);
}

@override
Expand All @@ -208,27 +237,98 @@ final class OctopusDelegate extends RouterDelegate<OctopusState>
if (configuration.children.isEmpty) return SynchronousFuture<void>(null);
return setNewRoutePath(configuration);
}

// Called when the one of the guards changed.
void _onGuardsNotified() {
setNewRoutePath(_stateObserver.value);
}

/// DO NOT USE THIS METHOD DIRECTLY.
/// Use [setNewRoutePath] instead.
/// Used for OctopusStateQueue.
///
/// {@nodoc}
@protected
@nonVirtual
Future<void> _setConfiguration(OctopusState configuration) =>
_handleErrors(() async {
var newConfiguration = configuration;
final history = _stateObserver.history;
for (final guard in _guards) {
try {
final result = await guard(history, newConfiguration);
if (result == null) return;
newConfiguration = result;
} on Object catch (error, stackTrace) {
developer.log(
'Guard ${guard.runtimeType} failed',
name: 'octopus',
error: error,
stackTrace: stackTrace,
level: 1000,
);
_onError?.call(error, stackTrace);
return; // Cancel navigation
}
}

if (_stateObserver._changeState(newConfiguration)) {
notifyListeners();
}
}, (_, __) => SynchronousFuture<void>(null));

@override
void dispose() {
_guardsListener.removeListener(_onGuardsNotified);
super.dispose();
}
}

/// Octopus state observer.
abstract interface class OctopusStateObserver<T extends OctopusState>
implements ValueListenable<T> {
/// Current immutable state.
@override
T get value;

/// History of the states.
List<OctopusState> get history;
}

final class _OctopusStateObserver
with ChangeNotifier
implements ValueListenable<OctopusState$Immutable> {
_OctopusStateObserver(OctopusState initialState)
: _value = OctopusState$Immutable.from(initialState);
implements OctopusStateObserver<OctopusState$Immutable> {
_OctopusStateObserver(OctopusState initialState,
[List<OctopusState>? history])
: _value = OctopusState$Immutable.from(initialState),
_history = history?.toList() ?? <OctopusState>[] {
// Add the initial state to the history.
if (_history.isEmpty || _history.last != initialState) {
_history.add(initialState);
}
}

@protected
@nonVirtual
OctopusState$Immutable _value;

@protected
@nonVirtual
final List<OctopusState> _history;

@override
List<OctopusState> get history =>
UnmodifiableListView<OctopusState>(_history);

@override
OctopusState$Immutable get value => _value;

@internal
@nonVirtual
void changeState(OctopusState? state) {
if (state == null) return;
if (state.children.isEmpty) return;
bool _changeState(OctopusState state) {
if (state.children.isEmpty) return false;
_value = OctopusState$Immutable.from(state);
_history.add(_value);
notifyListeners();
return true;
}
}
47 changes: 47 additions & 0 deletions lib/src/controller/guard.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:octopus/src/state/state.dart' show OctopusState;

/// Guard for the router.
///
/// {@template guard}
/// This is good place for checking permissions, authentication, etc.
///
/// Return the new state or null to cancel navigation.
/// If the returned state is null or throw an error,
/// the router will not change the state at all.
///
/// You should return the same state if you don't want to change it and
/// continue navigation.
///
/// You should return the new state if you want to change it and
/// continue navigation.
///
/// You should return null if you want to cancel navigation.
///
/// If something changed in app state, you should notify the guard
/// and router rerun the all guards with current state.
/// {@endtemplate}
abstract interface class IOctopusGuard implements Listenable {
/// Called when the [OctopusState] changes.
///
/// [history] is the history of the [OctopusState] states.
/// [state] is the expected new state.
///
/// {@macro guard}
FutureOr<OctopusState?> call(List<OctopusState> history, OctopusState state);
}

/// {@macro guard}
class OctopusGuard with ChangeNotifier implements IOctopusGuard {
/// {@macro guard}
OctopusGuard();

@override
FutureOr<OctopusState?> call(
List<OctopusState> history,
OctopusState state,
) =>
state;
}
2 changes: 1 addition & 1 deletion lib/src/controller/information_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import 'package:octopus/src/controller/state_codec.dart';
import 'package:octopus/src/state/state.dart';
import 'package:octopus/src/state/state_codec.dart';

/// Converts between [RouteInformation] and [OctopusState].
/// {@nodoc}
Expand Down
Loading

0 comments on commit a402130

Please sign in to comment.