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
|
/// A delegate that is used by the [Router] widget to parse a route information
|
||||||
/// into a configuration of type T.
|
/// into a configuration of type T.
|
||||||
///
|
///
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Simple router basic functionality - synchronized', (WidgetTester tester) async {
|
testWidgets('Simple router basic functionality - synchronized', (WidgetTester tester) async {
|
||||||
@ -726,6 +726,312 @@ testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('popped'), findsOneWidget);
|
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) {
|
Widget buildBoilerPlate(Widget child) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user