Skip to content

Commit

Permalink
Refactors page API (#137792)
Browse files Browse the repository at this point in the history
fixes flutter/flutter#137458

Chagnes:
1. Navigator.pop will always pop page based route
2. add a onDidRemovePage callback to replace onPopPage
3. Page.canPop and Page.onPopInvoked mirrors the PopScope, but in Page class.

migration guide flutter/website#10523
  • Loading branch information
chunhtai authored May 13, 2024
1 parent 27f683d commit a36ff80
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,10 @@ class _BottomNavTabState extends State<_BottomNavTab> {
},
child: Navigator(
key: _navigatorKey,
onPopPage: (Route<void> route, void result) {
if (!route.didPop(null)) {
return false;
}
onDidRemovePage: (Page<Object?> page) {
widget.onChangedPages(<_TabPage>[
...widget.pages,
]..removeLast());
return true;
},
pages: widget.pages.map((_TabPage page) {
switch (page) {
Expand Down
160 changes: 160 additions & 0 deletions examples/api/lib/widgets/page/page_can_pop.0.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This sample demonstrates showing a confirmation dialog before navigating
// away from a page.

import 'package:flutter/material.dart';

void main() => runApp(const PageApiExampleApp());

class PageApiExampleApp extends StatefulWidget {
const PageApiExampleApp({super.key});

@override
State<PageApiExampleApp> createState() => _PageApiExampleAppState();
}

class _PageApiExampleAppState extends State<PageApiExampleApp> {
final RouterDelegate<Object> delegate = MyRouterDelegate();

@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: delegate,
);
}
}

class MyRouterDelegate extends RouterDelegate<Object> with PopNavigatorRouterDelegateMixin<Object>, ChangeNotifier {
// This example doesn't use RouteInformationProvider.
@override
Future<void> setNewRoutePath(Object configuration) async => throw UnimplementedError();

@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

static MyRouterDelegate of(BuildContext context) => Router.of(context).routerDelegate as MyRouterDelegate;

bool get showDetailPage => _showDetailPage;
bool _showDetailPage = false;
set showDetailPage(bool value) {
if (_showDetailPage == value) {
return;
}
_showDetailPage = value;
notifyListeners();
}

Future<bool> _showConfirmDialog() async {
return await showDialog<bool>(
context: navigatorKey.currentContext!,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Are you sure?'),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
TextButton(
child: const Text('Confirm'),
onPressed: () {
Navigator.of(context).pop(true);
},
),
],
);
},
) ?? false;
}

Future<void> _handlePopDetails(bool didPop, void result) async {
if (didPop) {
showDetailPage = false;
return;
}
final bool confirmed = await _showConfirmDialog();
if (confirmed) {
showDetailPage = false;
}
}

List<Page<Object?>> _getPages() {
return <Page<Object?>>[
const MaterialPage<void>(key: ValueKey<String>('home'), child: _HomePage()),
if (showDetailPage)
MaterialPage<void>(
key: const ValueKey<String>('details'),
child: const _DetailsPage(),
canPop: false,
onPopInvoked: _handlePopDetails,
),
];
}

@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: _getPages(),
onDidRemovePage: (Page<Object?> page) {
assert(page.key == const ValueKey<String>('details'));
showDetailPage = false;
},
);
}
}

class _HomePage extends StatefulWidget {
const _HomePage();

@override
State<_HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<_HomePage> {

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: TextButton(
onPressed: () {
MyRouterDelegate.of(context).showDetailPage = true;
},
child: const Text('Go to details'),
),
),
);
}
}

class _DetailsPage extends StatefulWidget {
const _DetailsPage();

@override
State<_DetailsPage> createState() => _DetailsPageState();
}

class _DetailsPageState extends State<_DetailsPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Details')),
body: Center(
child: TextButton(
onPressed: () {
Navigator.of(context).maybePop();
},
child: const Text('Go back'),
),
),
);
}
}
54 changes: 54 additions & 0 deletions examples/api/test/widgets/page/page_can_pop.0_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter_api_samples/widgets/page/page_can_pop.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';

import '../navigator_utils.dart';

void main() {
testWidgets('Can choose to stay on page', (WidgetTester tester) async {
await tester.pumpWidget(
const example.PageApiExampleApp(),
);

expect(find.text('Home'), findsOneWidget);

await tester.tap(find.text('Go to details'));
await tester.pumpAndSettle();
expect(find.text('Home'), findsNothing);
expect(find.text('Details'), findsOneWidget);

await simulateSystemBack();
await tester.pumpAndSettle();
expect(find.text('Are you sure?'), findsOneWidget);

await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
expect(find.text('Home'), findsNothing);
expect(find.text('Details'), findsOneWidget);
});

testWidgets('Can choose to go back', (WidgetTester tester) async {
await tester.pumpWidget(
const example.PageApiExampleApp(),
);

expect(find.text('Home'), findsOneWidget);

await tester.tap(find.text('Go to details'));
await tester.pumpAndSettle();
expect(find.text('Home'), findsNothing);
expect(find.text('Details'), findsOneWidget);

await simulateSystemBack();
await tester.pumpAndSettle();
expect(find.text('Are you sure?'), findsOneWidget);

await tester.tap(find.text('Confirm'));
await tester.pumpAndSettle();
expect(find.text('Details'), findsNothing);
expect(find.text('Home'), findsOneWidget);
});
}
2 changes: 2 additions & 0 deletions packages/flutter/lib/src/cupertino/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,8 @@ class CupertinoPage<T> extends Page<T> {
this.title,
this.fullscreenDialog = false,
this.allowSnapshotting = true,
super.canPop,
super.onPopInvoked,
super.key,
super.name,
super.arguments,
Expand Down
2 changes: 2 additions & 0 deletions packages/flutter/lib/src/material/page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ class MaterialPage<T> extends Page<T> {
this.fullscreenDialog = false,
this.allowSnapshotting = true,
super.key,
super.canPop,
super.onPopInvoked,
super.name,
super.arguments,
super.restorationId,
Expand Down
Loading

0 comments on commit a36ff80

Please sign in to comment.