diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 87ec4b591ce..f118806243f 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.4.2 +- Adds `onPushRoute` callback to **GoRouter** that can be used to intercept route pushes from the application host. + ## 6.4.1 - Adds `initialExtra` to **GoRouter** to pass extra data alongside `initialRoute`. diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart index bcf426fcc13..be3d28e6508 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -8,6 +8,7 @@ library go_router; export 'src/configuration.dart' show GoRoute, GoRouterState, RouteBase, ShellRoute; +export 'src/information_provider.dart' show PushRouteDecision; export 'src/misc/extensions.dart'; export 'src/misc/inherited_router.dart'; export 'src/pages/custom_transition_page.dart'; diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index 7d8ee4f2ab3..96800d11ffa 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -2,23 +2,46 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; +import 'dart:async'; + import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +/// The decision on how to handle the route +/// when host tells the application to push a new one. +enum PushRouteDecision { + /// Delegate the route information to [WidgetsBindingObserver.didPushRoute], + /// in registration order, until one returns true. + delegate, + + /// Prevent the route information from being addressed by [GoRouter]. + prevent, + + /// Delegate the route information to the [GoRouter]. + navigate, +} + +/// Signature for callbacks that report a pushed route information. +typedef PushRouteCallback = FutureOr Function( + RouteInformation routeInformation, +); + /// The [RouteInformationProvider] created by go_router. class GoRouteInformationProvider extends RouteInformationProvider with WidgetsBindingObserver, ChangeNotifier { /// Creates a [GoRouteInformationProvider]. GoRouteInformationProvider({ required RouteInformation initialRouteInformation, + PushRouteCallback? onPushRoute, Listenable? refreshListenable, }) : _refreshListenable = refreshListenable, + _onPushRoute = onPushRoute, _value = initialRouteInformation { _refreshListenable?.addListener(notifyListeners); } final Listenable? _refreshListenable; + final PushRouteCallback? _onPushRoute; // ignore: unnecessary_non_null_assertion static WidgetsBinding get _binding => WidgetsBinding.instance; @@ -58,13 +81,27 @@ class GoRouteInformationProvider extends RouteInformationProvider RouteInformation _valueInEngine = RouteInformation(location: _binding.platformDispatcher.defaultRouteName); - void _platformReportsNewRouteInformation(RouteInformation routeInformation) { - if (_value == routeInformation) { - return; + Future _platformReportsNewRouteInformation( + RouteInformation routeInformation, + ) async { + final PushRouteDecision decision = + await _onPushRoute?.call(routeInformation) ?? + PushRouteDecision.navigate; + + switch (decision) { + case PushRouteDecision.delegate: + return false; + case PushRouteDecision.prevent: + return true; + case PushRouteDecision.navigate: + assert(hasListeners); + if (_value != routeInformation) { + _value = routeInformation; + _valueInEngine = routeInformation; + notifyListeners(); + } + return true; } - _value = routeInformation; - _valueInEngine = routeInformation; - notifyListeners(); } @override @@ -94,15 +131,12 @@ class GoRouteInformationProvider extends RouteInformationProvider @override Future didPushRouteInformation(RouteInformation routeInformation) { - assert(hasListeners); - _platformReportsNewRouteInformation(routeInformation); - return SynchronousFuture(true); + return _platformReportsNewRouteInformation(routeInformation); } @override Future didPushRoute(String route) { - assert(hasListeners); - _platformReportsNewRouteInformation(RouteInformation(location: route)); - return SynchronousFuture(true); + final RouteInformation routeInformation = RouteInformation(location: route); + return _platformReportsNewRouteInformation(routeInformation); } } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 29603720112..d38d139d438 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -64,6 +64,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig { bool debugLogDiagnostics = false, GlobalKey? navigatorKey, String? restorationScopeId, + PushRouteCallback? onPushRoute, }) : backButtonDispatcher = RootBackButtonDispatcher(), assert( initialExtra == null || initialLocation != null, @@ -91,6 +92,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig { location: _effectiveInitialLocation(initialLocation), state: initialExtra, ), + onPushRoute: onPushRoute, refreshListenable: refreshListenable, ); diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index d4d4b29c454..5e2a4f1163f 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 6.4.1 +version: 6.4.2 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 17335136bd7..adc797e5b69 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -3380,6 +3380,118 @@ void main() { }, ); }); + + group('push route decision', () { + testWidgets( + 'defaults to "navigate"', + (WidgetTester tester) async { + final List routes = [ + GoRoute( + path: '/', + builder: (_, __) => const SizedBox(), + ), + GoRoute( + path: '/dummy', + builder: (_, __) => const _DidPushRouteWidget(), + ), + ]; + + final GoRouter router = await createRouter(routes, tester); + + sendPlatformUrl('/dummy'); + await tester.pumpAndSettle(); + + expect(router.location, '/dummy'); + expect(find.text('DidPushRoute: null'), findsOneWidget); + }, + ); + + testWidgets( + 'based on location', + (WidgetTester tester) async { + final List routes = [ + GoRoute( + path: '/', + builder: (_, __) => const _DidPushRouteWidget(), + ), + GoRoute( + path: '/dummy', + builder: (_, __) => const _DidPushRouteWidget(), + ), + ]; + + final GoRouter router = await createRouter( + routes, + tester, + onPushRoute: (RouteInformation routeInformation) { + final String? location = routeInformation.location; + + switch (location) { + case '/prevent': + return PushRouteDecision.prevent; + case '/delegate': + return PushRouteDecision.delegate; + } + + return PushRouteDecision.navigate; + }, + ); + + sendPlatformUrl('/dummy'); + await tester.pumpAndSettle(); + expect(router.location, '/dummy'); + expect(find.text('DidPushRoute: null'), findsOneWidget); + + sendPlatformUrl('/prevent'); + await tester.pumpAndSettle(); + expect(router.location, '/dummy'); + expect(find.text('DidPushRoute: null'), findsOneWidget); + + sendPlatformUrl('/delegate'); + await tester.pumpAndSettle(); + expect(router.location, '/dummy'); + expect(find.text('DidPushRoute: /delegate'), findsOneWidget); + }, + ); + }); +} + +class _DidPushRouteWidget extends StatefulWidget { + const _DidPushRouteWidget(); + + @override + State<_DidPushRouteWidget> createState() => _DidPushRouteWidgetState(); +} + +class _DidPushRouteWidgetState extends State<_DidPushRouteWidget> + with WidgetsBindingObserver { + String? _route; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Future didPushRoute(String route) { + if (mounted) { + _route = route; + setState(() {}); + } + return SynchronousFuture(true); + } + + @override + Widget build(BuildContext context) { + return Text('DidPushRoute: $_route'); + } } /// This allows a value of type T or T? to be treated as a value of type T?. diff --git a/packages/go_router/test/information_provider_test.dart b/packages/go_router/test/information_provider_test.dart index e6ce0945508..7c98f00f509 100644 --- a/packages/go_router/test/information_provider_test.dart +++ b/packages/go_router/test/information_provider_test.dart @@ -13,18 +13,50 @@ void main() { group('GoRouteInformationProvider', () { testWidgets('notifies its listeners when set by the app', (WidgetTester tester) async { - late final GoRouteInformationProvider provider = - GoRouteInformationProvider(initialRouteInformation: initialRoute); + final GoRouteInformationProvider provider = GoRouteInformationProvider( + initialRouteInformation: initialRoute, + ); provider.addListener(expectAsync0(() {})); provider.value = newRoute; }); testWidgets('notifies its listeners when set by the platform', (WidgetTester tester) async { - late final GoRouteInformationProvider provider = - GoRouteInformationProvider(initialRouteInformation: initialRoute); + final GoRouteInformationProvider provider = GoRouteInformationProvider( + initialRouteInformation: initialRoute, + ); provider.addListener(expectAsync0(() {})); provider.didPushRouteInformation(newRoute); }); + + group('[push route decision]', () { + test('didPushRoute is false for "delegate"', () async { + final GoRouteInformationProvider provider = GoRouteInformationProvider( + initialRouteInformation: initialRoute, + onPushRoute: (_) => PushRouteDecision.delegate, + ); + expect(await provider.didPushRoute('/new'), isFalse); + expect(await provider.didPushRouteInformation(newRoute), isFalse); + }); + + test('didPushRoute is true for "prevent"', () async { + final GoRouteInformationProvider provider = GoRouteInformationProvider( + initialRouteInformation: initialRoute, + onPushRoute: (_) => PushRouteDecision.prevent, + ); + expect(await provider.didPushRoute('/new'), isTrue); + expect(await provider.didPushRouteInformation(newRoute), isTrue); + }); + + test('didPushRoute is true for "navigate"', () async { + final GoRouteInformationProvider provider = GoRouteInformationProvider( + initialRouteInformation: initialRoute, + onPushRoute: (_) => PushRouteDecision.navigate, + ); + provider.addListener(expectAsync0(() {})); + expect(await provider.didPushRoute('/new'), isTrue); + expect(await provider.didPushRouteInformation(newRoute), isTrue); + }); + }); }); } diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 04d12e7383e..e9faf81f324 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; +import 'package:go_router/src/information_provider.dart'; Future createGoRouter(WidgetTester tester) async { final GoRouter goRouter = GoRouter( @@ -145,6 +146,7 @@ Future createRouter( int redirectLimit = 5, GlobalKey? navigatorKey, GoRouterWidgetBuilder? errorBuilder, + PushRouteCallback? onPushRoute, }) async { final GoRouter goRouter = GoRouter( routes: routes, @@ -156,6 +158,7 @@ Future createRouter( (BuildContext context, GoRouterState state) => TestErrorScreen(state.error!), navigatorKey: navigatorKey, + onPushRoute: onPushRoute, ); await tester.pumpWidget( MaterialApp.router(