diff --git a/packages/flutter/lib/src/widgets/router.dart b/packages/flutter/lib/src/widgets/router.dart index 8465f2e908..aca00821c2 100644 --- a/packages/flutter/lib/src/widgets/router.dart +++ b/packages/flutter/lib/src/widgets/router.dart @@ -998,6 +998,74 @@ class ChildBackButtonDispatcher extends BackButtonDispatcher { } } +/// A convenience widget that registers a callback for when the back button is pressed. +/// +/// In order to use this widget, there must be an ancestor [Router] widget in the tree +/// that has a [RootBackButtonDispatcher]. e.g. The [Router] widget created by the +/// [MaterialApp.router] has a built-in [RootBackButtonDispatcher] by default. +/// +/// It only applies to platforms that accept back button clicks, such as Android. +/// +/// It can be useful for scenarios, in which you create a different state in your +/// screen but don't want to use a new page for that. +class BackButtonListener extends StatefulWidget { + /// Creates a BackButtonListener widget . + /// + /// The [child] and [onBackButtonPressed] arguments must not be null. + const BackButtonListener({ + Key? key, + required this.child, + required this.onBackButtonPressed, + }) : super(key: key); + + /// The widget below this widget in the tree. + final Widget child; + + /// The callback function that will be called when the back button is pressed. + /// + /// It must return a boolean future with true if this child will handle the request; + /// otherwise, return a boolean future with false. + final ValueGetter> onBackButtonPressed; + + @override + _BackButtonListenerState createState() => _BackButtonListenerState(); +} + +class _BackButtonListenerState extends State { + BackButtonDispatcher? dispatcher; + + @override + void didChangeDependencies() { + dispatcher?.removeCallback(widget.onBackButtonPressed); + + final BackButtonDispatcher? rootBackDispatcher = Router.of(context).backButtonDispatcher; + assert(rootBackDispatcher != null, 'The parent router must have a backButtonDispatcher to use this widget'); + + dispatcher = rootBackDispatcher!.createChildBackButtonDispatcher() + ..addCallback(widget.onBackButtonPressed) + ..takePriority(); + super.didChangeDependencies(); + } + + @override + void didUpdateWidget(covariant BackButtonListener oldWidget) { + if (oldWidget.onBackButtonPressed != widget.onBackButtonPressed) { + dispatcher?.removeCallback(oldWidget.onBackButtonPressed); + dispatcher?.addCallback(widget.onBackButtonPressed); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + dispatcher?.removeCallback(widget.onBackButtonPressed); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.child; +} + /// A delegate that is used by the [Router] widget to parse a route information /// into a configuration of type T. /// diff --git a/packages/flutter/test/widgets/router_test.dart b/packages/flutter/test/widgets/router_test.dart index 8869030236..55532a9e54 100644 --- a/packages/flutter/test/widgets/router_test.dart +++ b/packages/flutter/test/widgets/router_test.dart @@ -3,9 +3,9 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Simple router basic functionality - synchronized', (WidgetTester tester) async { @@ -726,6 +726,312 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester await tester.pump(); expect(find.text('popped'), findsOneWidget); }); + + testWidgets('BackButtonListener takes priority over root back dispatcher', (WidgetTester tester) async { + final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(); + provider.value = const RouteInformation( + location: 'initial', + ); + final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); + await tester.pumpWidget(buildBoilerPlate( + Router( + backButtonDispatcher: outerDispatcher, + routeInformationProvider: provider, + routeInformationParser: SimpleRouteInformationParser(), + routerDelegate: SimpleRouterDelegate( + builder: (BuildContext context, RouteInformation? information) { + // Creates the sub-router. + return Column( + children: [ + Text(information!.location!), + BackButtonListener( + child: Container(), + onBackButtonPressed: () { + provider.value = const RouteInformation( + location: 'popped inner1', + ); + return SynchronousFuture(true); + }, + ), + ], + ); + }, + onPopRoute: () { + provider.value = const RouteInformation( + location: 'popped outter', + ); + return SynchronousFuture(true); + } + ), + ) + )); + expect(find.text('initial'), findsOneWidget); + + bool result = false; + result = await outerDispatcher.invokeCallback(SynchronousFuture(false)); + expect(result, isTrue); + await tester.pump(); + expect(find.text('popped inner1'), findsOneWidget); + }); + + testWidgets('BackButtonListener updates callback if it has been changed', (WidgetTester tester) async { + final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(); + provider.value = const RouteInformation( + location: 'initial', + ); + final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); + final SimpleRouterDelegate routerDelegate = SimpleRouterDelegate() + ..builder = (BuildContext context, RouteInformation? information) { + // Creates the sub-router. + return Column( + children: [ + Text(information!.location!), + BackButtonListener( + child: Container(), + onBackButtonPressed: () { + provider.value = const RouteInformation( + location: 'first callback', + ); + return SynchronousFuture(true); + }, + ), + ], + ); + } + ..onPopRoute = () { + provider.value = const RouteInformation( + location: 'popped outter', + ); + return SynchronousFuture(true); + }; + + await tester.pumpWidget(buildBoilerPlate( + Router( + backButtonDispatcher: outerDispatcher, + routeInformationProvider: provider, + routeInformationParser: SimpleRouteInformationParser(), + routerDelegate: routerDelegate + ) + )); + + routerDelegate + ..builder = (BuildContext context, RouteInformation? information) { + // Creates the sub-router. + return Column( + children: [ + Text(information!.location!), + BackButtonListener( + child: Container(), + onBackButtonPressed: () { + provider.value = const RouteInformation( + location: 'second callback', + ); + return SynchronousFuture(true); + }, + ), + ], + ); + } + ..onPopRoute = () { + provider.value = const RouteInformation( + location: 'popped outter', + ); + return SynchronousFuture(true); + }; + + await tester.pumpWidget(buildBoilerPlate( + Router( + backButtonDispatcher: outerDispatcher, + routeInformationProvider: provider, + routeInformationParser: SimpleRouteInformationParser(), + routerDelegate: routerDelegate, + ) + )); + await tester.pump(); + await outerDispatcher.invokeCallback(SynchronousFuture(false)); + await tester.pump(); + expect(find.text('second callback'), findsOneWidget); + }); + + testWidgets('BackButtonListener clears callback if it is disposed', (WidgetTester tester) async { + final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(); + provider.value = const RouteInformation( + location: 'initial', + ); + final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); + final SimpleRouterDelegate routerDelegate = SimpleRouterDelegate() + ..builder = (BuildContext context, RouteInformation? information) { + // Creates the sub-router. + return Column( + children: [ + Text(information!.location!), + BackButtonListener( + child: Container(), + onBackButtonPressed: () { + provider.value = const RouteInformation( + location: 'first callback', + ); + return SynchronousFuture(true); + }, + ), + ], + ); + } + ..onPopRoute = () { + provider.value = const RouteInformation( + location: 'popped outter', + ); + return SynchronousFuture(true); + }; + + await tester.pumpWidget(buildBoilerPlate( + Router( + backButtonDispatcher: outerDispatcher, + routeInformationProvider: provider, + routeInformationParser: SimpleRouteInformationParser(), + routerDelegate: routerDelegate + ) + )); + + routerDelegate + ..builder = (BuildContext context, RouteInformation? information) { + // Creates the sub-router. + return Column( + children: [ + Text(information!.location!), + ], + ); + } + ..onPopRoute = () { + provider.value = const RouteInformation( + location: 'popped outter', + ); + return SynchronousFuture(true); + }; + + await tester.pumpWidget(buildBoilerPlate( + Router( + backButtonDispatcher: outerDispatcher, + routeInformationProvider: provider, + routeInformationParser: SimpleRouteInformationParser(), + routerDelegate: routerDelegate, + ) + )); + await tester.pump(); + await outerDispatcher.invokeCallback(SynchronousFuture(false)); + await tester.pump(); + expect(find.text('popped outter'), findsOneWidget); + }); + + testWidgets('Nested backButtonListener should take priority', (WidgetTester tester) async { + final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(); + provider.value = const RouteInformation( + location: 'initial', + ); + final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); + await tester.pumpWidget(buildBoilerPlate( + Router( + backButtonDispatcher: outerDispatcher, + routeInformationProvider: provider, + routeInformationParser: SimpleRouteInformationParser(), + routerDelegate: SimpleRouterDelegate( + builder: (BuildContext context, RouteInformation? information) { + // Creates the sub-router. + return Column( + children: [ + Text(information!.location!), + BackButtonListener( + child: BackButtonListener( + child: Container(), + onBackButtonPressed: () { + provider.value = const RouteInformation( + location: 'popped inner2', + ); + return SynchronousFuture(true); + }, + ), + onBackButtonPressed: () { + provider.value = const RouteInformation( + location: 'popped inner1', + ); + return SynchronousFuture(true); + }, + ), + ], + ); + }, + onPopRoute: () { + provider.value = const RouteInformation( + location: 'popped outter', + ); + return SynchronousFuture(true); + } + ), + ) + )); + expect(find.text('initial'), findsOneWidget); + + bool result = false; + result = await outerDispatcher.invokeCallback(SynchronousFuture(false)); + expect(result, isTrue); + await tester.pump(); + expect(find.text('popped inner2'), findsOneWidget); + }); + + testWidgets('Nested backButtonListener that returns false should call next on the line', (WidgetTester tester) async { + final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider(); + provider.value = const RouteInformation( + location: 'initial', + ); + final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); + await tester.pumpWidget(buildBoilerPlate( + Router( + backButtonDispatcher: outerDispatcher, + routeInformationProvider: provider, + routeInformationParser: SimpleRouteInformationParser(), + routerDelegate: SimpleRouterDelegate( + builder: (BuildContext context, RouteInformation? information) { + // Creates the sub-router. + return Column( + children: [ + Text(information!.location!), + BackButtonListener( + child: BackButtonListener( + child: Container(), + onBackButtonPressed: () { + provider.value = const RouteInformation( + location: 'popped inner2', + ); + return SynchronousFuture(false); + }, + ), + onBackButtonPressed: () { + provider.value = const RouteInformation( + location: 'popped inner1', + ); + return SynchronousFuture(true); + }, + ), + ], + ); + }, + onPopRoute: () { + provider.value = const RouteInformation( + location: 'popped outter', + ); + return SynchronousFuture(true); + } + ), + ) + )); + expect(find.text('initial'), findsOneWidget); + + bool result = false; + result = await outerDispatcher.invokeCallback(SynchronousFuture(false)); + expect(result, isTrue); + await tester.pump(); + expect(find.text('popped inner1'), findsOneWidget); + }); } Widget buildBoilerPlate(Widget child) {