Unverified Commit 7cec142f authored by Douglas Iacovelli's avatar Douglas Iacovelli Committed by GitHub

Add back button listener widget (#79642)

parent 4bf26b68
......@@ -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<Future<bool>> onBackButtonPressed;
@override
_BackButtonListenerState createState() => _BackButtonListenerState();
}
class _BackButtonListenerState extends State<BackButtonListener> {
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.
///
......
......@@ -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<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation? information) {
// Creates the sub-router.
return Column(
children: <Widget>[
Text(information!.location!),
BackButtonListener(
child: Container(),
onBackButtonPressed: () {
provider.value = const RouteInformation(
location: 'popped inner1',
);
return SynchronousFuture<bool>(true);
},
),
],
);
},
onPopRoute: () {
provider.value = const RouteInformation(
location: 'popped outter',
);
return SynchronousFuture<bool>(true);
}
),
)
));
expect(find.text('initial'), findsOneWidget);
bool result = false;
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(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: <Widget>[
Text(information!.location!),
BackButtonListener(
child: Container(),
onBackButtonPressed: () {
provider.value = const RouteInformation(
location: 'first callback',
);
return SynchronousFuture<bool>(true);
},
),
],
);
}
..onPopRoute = () {
provider.value = const RouteInformation(
location: 'popped outter',
);
return SynchronousFuture<bool>(true);
};
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: routerDelegate
)
));
routerDelegate
..builder = (BuildContext context, RouteInformation? information) {
// Creates the sub-router.
return Column(
children: <Widget>[
Text(information!.location!),
BackButtonListener(
child: Container(),
onBackButtonPressed: () {
provider.value = const RouteInformation(
location: 'second callback',
);
return SynchronousFuture<bool>(true);
},
),
],
);
}
..onPopRoute = () {
provider.value = const RouteInformation(
location: 'popped outter',
);
return SynchronousFuture<bool>(true);
};
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: routerDelegate,
)
));
await tester.pump();
await outerDispatcher.invokeCallback(SynchronousFuture<bool>(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: <Widget>[
Text(information!.location!),
BackButtonListener(
child: Container(),
onBackButtonPressed: () {
provider.value = const RouteInformation(
location: 'first callback',
);
return SynchronousFuture<bool>(true);
},
),
],
);
}
..onPopRoute = () {
provider.value = const RouteInformation(
location: 'popped outter',
);
return SynchronousFuture<bool>(true);
};
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: routerDelegate
)
));
routerDelegate
..builder = (BuildContext context, RouteInformation? information) {
// Creates the sub-router.
return Column(
children: <Widget>[
Text(information!.location!),
],
);
}
..onPopRoute = () {
provider.value = const RouteInformation(
location: 'popped outter',
);
return SynchronousFuture<bool>(true);
};
await tester.pumpWidget(buildBoilerPlate(
Router<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: routerDelegate,
)
));
await tester.pump();
await outerDispatcher.invokeCallback(SynchronousFuture<bool>(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<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation? information) {
// Creates the sub-router.
return Column(
children: <Widget>[
Text(information!.location!),
BackButtonListener(
child: BackButtonListener(
child: Container(),
onBackButtonPressed: () {
provider.value = const RouteInformation(
location: 'popped inner2',
);
return SynchronousFuture<bool>(true);
},
),
onBackButtonPressed: () {
provider.value = const RouteInformation(
location: 'popped inner1',
);
return SynchronousFuture<bool>(true);
},
),
],
);
},
onPopRoute: () {
provider.value = const RouteInformation(
location: 'popped outter',
);
return SynchronousFuture<bool>(true);
}
),
)
));
expect(find.text('initial'), findsOneWidget);
bool result = false;
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(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<RouteInformation>(
backButtonDispatcher: outerDispatcher,
routeInformationProvider: provider,
routeInformationParser: SimpleRouteInformationParser(),
routerDelegate: SimpleRouterDelegate(
builder: (BuildContext context, RouteInformation? information) {
// Creates the sub-router.
return Column(
children: <Widget>[
Text(information!.location!),
BackButtonListener(
child: BackButtonListener(
child: Container(),
onBackButtonPressed: () {
provider.value = const RouteInformation(
location: 'popped inner2',
);
return SynchronousFuture<bool>(false);
},
),
onBackButtonPressed: () {
provider.value = const RouteInformation(
location: 'popped inner1',
);
return SynchronousFuture<bool>(true);
},
),
],
);
},
onPopRoute: () {
provider.value = const RouteInformation(
location: 'popped outter',
);
return SynchronousFuture<bool>(true);
}
),
)
));
expect(find.text('initial'), findsOneWidget);
bool result = false;
result = await outerDispatcher.invokeCallback(SynchronousFuture<bool>(false));
expect(result, isTrue);
await tester.pump();
expect(find.text('popped inner1'), findsOneWidget);
});
}
Widget buildBoilerPlate(Widget child) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment