From 52e09e4b62462895d1ee8c3085185fca7219c2a9 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Sun, 19 Nov 2023 07:01:33 +0400 Subject: [PATCH] Add _buildPages method --- lib/src/controller/delegate.dart | 55 ++++++++++++++++++++++++++++-- lib/src/controller/octopus.dart | 5 +++ lib/src/state/state.dart | 34 ++++++++++++++----- lib/src/utils/state_util.dart | 58 ++++++++++++++++++-------------- 4 files changed, 115 insertions(+), 37 deletions(-) diff --git a/lib/src/controller/delegate.dart b/lib/src/controller/delegate.dart index 2ad45fc..5b50e7c 100644 --- a/lib/src/controller/delegate.dart +++ b/lib/src/controller/delegate.dart @@ -14,12 +14,16 @@ final class OctopusDelegate extends RouterDelegate /// {@nodoc} OctopusDelegate({ required OctopusState initialState, + required List routes, String? restorationScopeId = 'octopus', List? observers, TransitionDelegate? transitionDelegate, RouteFactory? notFound, void Function(Object error, StackTrace stackTrace)? onError, - }) : _restorationScopeId = restorationScopeId, + }) : _routes = { + for (final route in routes) route.name: route, + }, + _restorationScopeId = restorationScopeId, _observers = observers, _transitionDelegate = transitionDelegate ?? const DefaultTransitionDelegate(), @@ -46,6 +50,9 @@ final class OctopusDelegate extends RouterDelegate /// Current octopus instance. late Octopus _controller; + /// Routes hash table. + final Map _routes; + @internal set $controller(Octopus controller) => _controller = controller; @@ -92,14 +99,56 @@ final class OctopusDelegate extends RouterDelegate bool _onPopPage(Route route, Object? result) => _handleErrors( () { if (!route.didPop(result)) return false; - final popped = value.maybePop(); + final state = value.copy(); + final popped = state.maybePop(); if (popped == null) return false; - setNewRoutePath(popped); + setNewRoutePath(state); return true; }, (_, __) => false, ); + List> _buildPages(BuildContext context) => _handleErrors(() { + final pages = >[]; + for (final node in value.children) { + 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); + } + if (pages.isNotEmpty) return pages; + throw FlutterError('The Navigator.pages must not be empty to use the ' + 'Navigator.pages API'); + }, (error, stackTrace) { + final flutterError = switch (error) { + FlutterError error => error, + String message => FlutterError(message), + _ => FlutterError.fromParts( + [ + ErrorSummary('Failed to build pages'), + ErrorDescription(Error.safeToString(error)), + ], + ), + }; + return >[ + MaterialPage( + child: Scaffold( + body: SafeArea( + child: ErrorWidget.withDetails( + message: 'Failed to build pages', + error: flutterError, + ), + ), + ), + arguments: { + 'error': Error.safeToString(error), + 'stack': stackTrace.toString(), + }, + ), + ]; + }); + @override Future popRoute() => _handleErrors(() { final nav = navigator; diff --git a/lib/src/controller/octopus.dart b/lib/src/controller/octopus.dart index 93c9041..bcbbb3f 100644 --- a/lib/src/controller/octopus.dart +++ b/lib/src/controller/octopus.dart @@ -64,6 +64,10 @@ final class _OctopusImpl extends Octopus onError?.call(error, StackTrace.current); throw error; } + assert( + list.map((e) => e.name).toSet().length == list.length, + 'Routes list should not contain duplicate names', + ); final routeInformationProvider = OctopusInformationProvider(); final backButtonDispatcher = RootBackButtonDispatcher(); final routeInformationParser = OctopusInformationParser(); @@ -72,6 +76,7 @@ final class _OctopusImpl extends Octopus children: [defaultRoute.node()], arguments: {}, ), + routes: list, restorationScopeId: restorationScopeId, observers: observers, transitionDelegate: transitionDelegate, diff --git a/lib/src/state/state.dart b/lib/src/state/state.dart index 0da34be..2d4c995 100644 --- a/lib/src/state/state.dart +++ b/lib/src/state/state.dart @@ -1,8 +1,8 @@ import 'dart:collection'; +import 'package:flutter/material.dart' show MaterialPage; import 'package:flutter/widgets.dart'; import 'package:octopus/src/utils/state_util.dart'; -import 'package:octopus/src/widget/route_context.dart'; /// Signature for the callback to [OctopusNode.visitChildNodes]. /// @@ -117,18 +117,36 @@ abstract class OctopusRoute { /// e.g. my-page abstract final String name; - /// Build [Widget] for this route using [OctopusRouteContext]. - /// Use [OctopusRouteContext] to access current route information, + /// Build [Widget] for this route using [BuildContext] and [OctopusNode]. + /// + /// Use [OctopusNode] to access current route information, /// arguments and its children. /// /// e.g. /// ```dart - /// context.node; - /// context.name; - /// context.arguments; - /// context.children; + /// final OctopusNode(:name, :arguments, :children) = node; /// ``` - Page builder(OctopusRouteContext context); + Widget builder(BuildContext context, OctopusNode node); + + /// Build [Page] for this route using [BuildContext] and [OctopusNode]. + /// [BuildContext] - Navigator context. + /// [OctopusNode] - Current node of the router state tree. + Page pageBuilder(BuildContext context, OctopusNode node) { + final OctopusNode(:name, arguments: args) = node; + final key = ValueKey( + args.isEmpty + ? name + : '$name' + '#' + '${args.entries.map((e) => '${e.key}=${e.value}').join(';')}', + ); + return MaterialPage( + key: key, + child: builder(context, node), + name: name, + arguments: args, + ); + } /// Construct [OctopusNode] for this route. OctopusNode node({ diff --git a/lib/src/utils/state_util.dart b/lib/src/utils/state_util.dart index 995a011..b83e9ea 100644 --- a/lib/src/utils/state_util.dart +++ b/lib/src/utils/state_util.dart @@ -50,11 +50,17 @@ abstract final class StateUtil { final segments = []; void encodeNode(OctopusNode node, int depth) { final prefix = '.' * depth; - final args = node.arguments.entries - .map((e) => - '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') - .join('&'); - final name = args.isEmpty ? node.name : '${node.name}~$args'; + final String name; + if (node.arguments.isEmpty) { + name = node.name; + } else { + final args = (node.arguments.entries.toList(growable: false) + ..sort((a, b) => a.key.compareTo(b.key))) + .map((e) => + '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') + .join('&'); + name = args.isEmpty ? node.name : '${node.name}~$args'; + } segments.add('$prefix$name'); @@ -76,25 +82,15 @@ abstract final class StateUtil { /// Convert location string to tree components. /// {@nodoc} @internal - static OctopusState decodeLocation(String location) { - final arguments = {}; - final segments = - location.replaceAll('\n', '').replaceAll(r'\', '/').trim().split('/'); - if (segments.isEmpty) { - return OctopusState( - children: [], - arguments: arguments, - ); - } - - return OctopusState( - children: _parseSegments(segments, 0).toList(), - arguments: arguments, - ); - } + static OctopusState decodeLocation(String location) => + stateFromUri(Uri.parse(location)); static OctopusState stateFromUri(Uri uri) { - final arguments = uri.queryParameters; + final queryParameters = uri.queryParameters.entries.toList(growable: false) + ..sort((a, b) => a.key.compareTo(b.key)); + final arguments = { + for (final entry in queryParameters) entry.key: entry.value + }; final segments = uri.pathSegments; if (segments.isEmpty) { return OctopusState( @@ -170,13 +166,23 @@ abstract final class StateUtil { segment = segment.substring(currentDepth); final delimiter = segment.indexOf('~'); final name = delimiter == -1 ? segment : segment.substring(0, delimiter); - final args = delimiter == -1 - ? {} - : Uri.splitQueryString(segment.substring(delimiter + 1)); + final Map arguments; + if (delimiter == -1) { + arguments = {}; + } else { + final queryParameters = + Uri.splitQueryString(segment.substring(delimiter + 1)) + .entries + .toList(growable: false) + ..sort((a, b) => a.key.compareTo(b.key)); + arguments = { + for (final entry in queryParameters) entry.key: entry.value + }; + } var children = currentDepth < segment.length - 1 ? _parseSegments(segments, currentDepth + 1).toList() : []; - yield OctopusNode(name: name, arguments: args, children: children); + yield OctopusNode(name: name, arguments: arguments, children: children); } } }