diff --git a/packages/flutter/lib/src/widgets/router.dart b/packages/flutter/lib/src/widgets/router.dart index c8bfdf92d8..b501f515a4 100644 --- a/packages/flutter/lib/src/widgets/router.dart +++ b/packages/flutter/lib/src/widgets/router.dart @@ -494,6 +494,7 @@ class _RouterState extends State> with RestorationMixin { Object? _currentRouterTransaction; RouteInformationReportingType? _currentIntentionToReport; final _RestorableRouteInformation _routeInformation = _RestorableRouteInformation(); + late bool _routeParsePending; @override String? get restorationId => widget.restorationScopeId; @@ -580,7 +581,15 @@ class _RouterState extends State> with RestorationMixin { @override void didChangeDependencies() { + _routeParsePending = true; super.didChangeDependencies(); + // The super.didChangeDependencies may have parsed the route information. + // This can happen if the didChangeDependencies is triggered by state + // restoration or first build. + if (widget.routeInformationProvider != null && _routeParsePending) { + _processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setNewRoutePath); + } + _routeParsePending = false; _maybeNeedToReportRouteInformation(); } @@ -621,9 +630,11 @@ class _RouterState extends State> with RestorationMixin { } void _processRouteInformation(RouteInformation information, ValueGetter<_RouteSetter> delegateRouteSetter) { + assert(_routeParsePending); + _routeParsePending = false; _currentRouterTransaction = Object(); widget.routeInformationParser! - .parseRouteInformation(information) + .parseRouteInformationWithDependencies(information, context) .then(_processParsedRouteInformation(_currentRouterTransaction, delegateRouteSetter)); } @@ -641,6 +652,7 @@ class _RouterState extends State> with RestorationMixin { void _handleRouteInformationProviderNotification() { assert(widget.routeInformationProvider!.value != null); + _routeParsePending = true; _processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setNewRoutePath); } @@ -685,8 +697,10 @@ class _RouterState extends State> with RestorationMixin { routerDelegate: widget.routerDelegate, routerState: this, child: Builder( - // We use a Builder so that the build method below - // will have a BuildContext that contains the _RouterScope. + // Use a Builder so that the build method below will have a + // BuildContext that contains the _RouterScope. This also prevents + // dependencies look ups in routerDelegate from rebuilding Router + // widget that may result in re-parsing the route information. builder: widget.routerDelegate.build, ), ), @@ -1079,11 +1093,16 @@ class _BackButtonListenerState extends State { /// route information from [Router.routeInformationProvider] and any subsequent /// new route notifications from it. The [Router] widget calls the [parseRouteInformation] /// with the route information from [Router.routeInformationProvider]. +/// +/// One of the [parseRouteInformation] or +/// [parseRouteInformationWithDependencies] must be implemented, otherwise a +/// runtime error will be thrown. abstract class RouteInformationParser { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const RouteInformationParser(); + /// {@template flutter.widgets.RouteInformationParser.parseRouteInformation} /// Converts the given route information into parsed data to pass to a /// [RouterDelegate]. /// @@ -1094,7 +1113,30 @@ abstract class RouteInformationParser { /// Consider using a [SynchronousFuture] if the result can be computed /// synchronously, so that the [Router] does not need to wait for the next /// microtask to pass the data to the [RouterDelegate]. - Future parseRouteInformation(RouteInformation routeInformation); + /// {@endtemplate} + /// + /// One can implement [parseRouteInformationWithDependencies] instead if + /// the parsing depends on other dependencies from the [BuildContext]. + Future parseRouteInformation(RouteInformation routeInformation) { + throw UnimplementedError( + 'One of the parseRouteInformation or ' + 'parseRouteInformationWithDependencies must be implemented' + ); + } + + /// {@macro flutter.widgets.RouteInformationParser.parseRouteInformation} + /// + /// The input [BuildContext] can be used for looking up [InheritedWidget]s + /// If one uses [BuildContext.dependOnInheritedWidgetOfExactType], a + /// dependency will be created. The [Router] will reparse the + /// [RouteInformation] from its [RouteInformationProvider] if the dependency + /// notifies its listeners. + /// + /// One can also use [BuildContext.getElementForInheritedWidgetOfExactType] to + /// look up [InheritedWidget]s without creating dependencies. + Future parseRouteInformationWithDependencies(RouteInformation routeInformation, BuildContext context) { + return parseRouteInformation(routeInformation); + } /// Restore the route information from the given configuration. /// diff --git a/packages/flutter/test/widgets/router_test.dart b/packages/flutter/test/widgets/router_test.dart index f44d2d2569..2479f76bea 100644 --- a/packages/flutter/test/widgets/router_test.dart +++ b/packages/flutter/test/widgets/router_test.dart @@ -1308,6 +1308,168 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester expect(find.text('Current route: /404'), findsOneWidget); expect(reportedRouteInformation[1].location, '/404'); }); + + testWidgets('RouterInformationParser can look up dependencies and reparse', (WidgetTester tester) async { + final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(); + provider.value = const RouteInformation( + location: 'initial', + ); + final BackButtonDispatcher dispatcher = RootBackButtonDispatcher(); + int expectedMaxLines = 1; + bool parserCalled = false; + final Widget router = Router( + routeInformationProvider: provider, + routeInformationParser: CustomRouteInformationParser((RouteInformation information, BuildContext context) { + parserCalled = true; + final DefaultTextStyle style = DefaultTextStyle.of(context); + return RouteInformation(location: '${style.maxLines}'); + }), + routerDelegate: SimpleRouterDelegate( + builder: (BuildContext context, RouteInformation? information) { + return Text(information!.location!); + }, + onPopRoute: () { + provider.value = const RouteInformation( + location: 'popped', + ); + return SynchronousFuture(true); + }, + ), + backButtonDispatcher: dispatcher, + ); + await tester.pumpWidget(buildBoilerPlate( + DefaultTextStyle( + style: const TextStyle(), + maxLines: expectedMaxLines, + child: router, + ), + )); + + expect(find.text('$expectedMaxLines'), findsOneWidget); + expect(parserCalled, isTrue); + + parserCalled = false; + expectedMaxLines = 2; + await tester.pumpWidget(buildBoilerPlate( + DefaultTextStyle( + style: const TextStyle(), + maxLines: expectedMaxLines, + child: router, + ), + )); + await tester.pump(); + expect(find.text('$expectedMaxLines'), findsOneWidget); + expect(parserCalled, isTrue); + }); + + testWidgets('RouterInformationParser can look up dependencies without reparsing', (WidgetTester tester) async { + final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(); + provider.value = const RouteInformation( + location: 'initial', + ); + final BackButtonDispatcher dispatcher = RootBackButtonDispatcher(); + const int expectedMaxLines = 1; + bool parserCalled = false; + final Widget router = Router( + routeInformationProvider: provider, + routeInformationParser: CustomRouteInformationParser((RouteInformation information, BuildContext context) { + parserCalled = true; + final DefaultTextStyle style = context.getElementForInheritedWidgetOfExactType()!.widget as DefaultTextStyle; + return RouteInformation(location: '${style.maxLines}'); + }), + routerDelegate: SimpleRouterDelegate( + builder: (BuildContext context, RouteInformation? information) { + return Text(information!.location!); + }, + onPopRoute: () { + provider.value = const RouteInformation( + location: 'popped', + ); + return SynchronousFuture(true); + }, + ), + backButtonDispatcher: dispatcher, + ); + await tester.pumpWidget(buildBoilerPlate( + DefaultTextStyle( + style: const TextStyle(), + maxLines: expectedMaxLines, + child: router, + ), + )); + + expect(find.text('$expectedMaxLines'), findsOneWidget); + expect(parserCalled, isTrue); + + parserCalled = false; + const int newMaxLines = 2; + // This rebuild should not trigger re-parsing. + await tester.pumpWidget(buildBoilerPlate( + DefaultTextStyle( + style: const TextStyle(), + maxLines: newMaxLines, + child: router, + ), + )); + await tester.pump(); + expect(find.text('$newMaxLines'), findsNothing); + expect(find.text('$expectedMaxLines'), findsOneWidget); + expect(parserCalled, isFalse); + }); + + testWidgets('Looks up dependencies in RouterDelegate does not trigger re-parsing', (WidgetTester tester) async { + final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(); + provider.value = const RouteInformation( + location: 'initial', + ); + final BackButtonDispatcher dispatcher = RootBackButtonDispatcher(); + int expectedMaxLines = 1; + bool parserCalled = false; + final Widget router = Router( + routeInformationProvider: provider, + routeInformationParser: CustomRouteInformationParser((RouteInformation information, BuildContext context) { + parserCalled = true; + return information; + }), + routerDelegate: SimpleRouterDelegate( + builder: (BuildContext context, RouteInformation? information) { + final DefaultTextStyle style = DefaultTextStyle.of(context); + return Text('${style.maxLines}'); + }, + onPopRoute: () { + provider.value = const RouteInformation( + location: 'popped', + ); + return SynchronousFuture(true); + }, + ), + backButtonDispatcher: dispatcher, + ); + await tester.pumpWidget(buildBoilerPlate( + DefaultTextStyle( + style: const TextStyle(), + maxLines: expectedMaxLines, + child: router, + ), + )); + + expect(find.text('$expectedMaxLines'), findsOneWidget); + // Initial route will be parsed regardless. + expect(parserCalled, isTrue); + + parserCalled = false; + expectedMaxLines = 2; + await tester.pumpWidget(buildBoilerPlate( + DefaultTextStyle( + style: const TextStyle(), + maxLines: expectedMaxLines, + child: router, + ), + )); + await tester.pump(); + expect(find.text('$expectedMaxLines'), findsOneWidget); + expect(parserCalled, isFalse); + }); } Widget buildBoilerPlate(Widget child) { @@ -1322,6 +1484,7 @@ typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInforma typedef SimpleRouterDelegatePopRoute = Future Function(); typedef SimpleNavigatorRouterDelegatePopPage = bool Function(Route route, T result); typedef RouterReportRouterInformation = void Function(RouteInformation, RouteInformationReportingType); +typedef CustomRouteInformationParserCallback = RouteInformation Function(RouteInformation, BuildContext); class SimpleRouteInformationParser extends RouteInformationParser { SimpleRouteInformationParser(); @@ -1337,6 +1500,22 @@ class SimpleRouteInformationParser extends RouteInformationParser { + const CustomRouteInformationParser(this.callback); + + final CustomRouteInformationParserCallback callback; + + @override + Future parseRouteInformationWithDependencies(RouteInformation information, BuildContext context) { + return SynchronousFuture(callback(information, context)); + } + + @override + RouteInformation restoreRouteInformation(RouteInformation configuration) { + return configuration; + } +} + class SimpleRouterDelegate extends RouterDelegate with ChangeNotifier { SimpleRouterDelegate({ this.builder,