Add back button listener widget (#79642)
This commit is contained in:
parent
4bf26b6801
commit
7cec142f8c
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user