阅读大概需要9分钟
本文介绍了flutter中Navigator
和Router
API是如何工作的。如果你一直在关注 Flutter 开放的设计文档[1],你可能已经看到了这些称为Navigator 2.0 和 Router 的[2]新功能。下面我们将探索这些 API 如何对应用中的视觉进行更精细的控制,以及如何使用它来解析路由。
这些新的 API 并没有破坏性的变化,只是添加了一个新的_声明性_API[3]。在 Navigator 2.0 之前,很难推送或弹出多个页面[4],或者删除当前页面下方的页面。但是,如果对Navigator
的工作方式感到满意,也可以继续方式使用它。
Router
提供了从底层平台处理方和显示相应页面的方法。在本文中,我们使用Router
去解析浏览器 URL 并且显示相应的页面。
学完本文后,你将找到在你的APP中使用Navigator
最好方式,并且可以掌握如何使用 Navigator 2.0 来解析浏览器 URL 并能完全控制激活中的页面栈。本文将通过一个示例来演示如何处理平台传入的路由并管理APP的页面。
Navigator 1.0
在 Flutter中,你一定知道Navigator
的以下概念:
[**Navigator**](https://master-api.flutter.dev/flutter/widgets/Navigator-class.html "**Navigator**")
— 管理一组 Route 对象的小部件。[**Route**](https://master-api.flutter.dev/flutter/widgets/Route-class.html "**Route**")
— 被Navigator
管理的当前屏幕,Route的实现比如MaterialPageRoute
.
在 Navigator 2.0 之前,页面使用【命名路由】或【匿名路由】进栈和出栈。接下来的部分是对这两种方法做一个简要的回顾。
匿名路由
在flutter中通过Navigator
可以很轻松的实现路由管理.
在MaterialApp
和CupertinoApp
使用Navigator
非常容易。可以使用[Navigator.of()](https://api.flutter.dev/flutter/widgets/Navigator/of.html "Navigator.of( "Navigator.of()")")
或使用[Navigator.push()](https://api.flutter.dev/flutter/widgets/Navigator/push.html "Navigator.push( "Navigator.push()")")
显示一个新页面,或者使用[Navigator.pop()](https://api.flutter.dev/flutter/services/SystemNavigator/pop.html "Navigator.pop( "Navigator.pop()")")
返回上一个页面。
import 'package:flutter/material.dart';
void main() {
runApp(Nav2App());
}
class Nav2App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('View Details'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return DetailScreen();
}),
);
},
),
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('Pop!'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}
当push()
被调用时,DetailScreen
被放置在HomeScreen
顶部,如下所示:
前一个页面( HomeScreen
) 仍然是widget树的一部分,因此与State
相关的都不会被销毁。
命名路由
Flutter 还支持命名路由,在MaterialApp
或CupertinoApp
的routes
参数中进行定义 :
import 'package:flutter/material.dart';
void main() {
runApp(Nav2App());
}
class Nav2App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailScreen(),
},
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('View Details'),
onPressed: () {
Navigator.pushNamed(
context,
'/details',
);
},
),
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('Pop!'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}
这些路由必须预先定义。尽管可以将参数传递给命名的路由[5],但无法解析路由本身的参数。如/details/:id
。
使用 onGenerateRoute 的高级命名路由
处理命名路由的一种更灵活的方法是使用onGenerateRoute
. 此 API 使您能够处理所有路径:
onGenerateRoute: (settings) {
// Handle '/'
if (settings.name == '/') {
return MaterialPageRoute(builder: (context) => HomeScreen());
}
// Handle '/details/:id'
var uri = Uri.parse(settings.name);
if (uri.pathSegments.length == 2 &&
uri.pathSegments.first == 'details') {
var id = uri.pathSegments[1];
return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
}
return MaterialPageRoute(builder: (context) => UnknownScreen());
},
这是完整的示例:
代码语言:javascript复制import 'package:flutter/material.dart';
void main() {
runApp(Nav2App());
}
class Nav2App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: (settings) {
// Handle '/'
if (settings.name == '/') {
return MaterialPageRoute(builder: (context) => HomeScreen());
}
// Handle '/details/:id'
var uri = Uri.parse(settings.name);
if (uri.pathSegments.length == 2 &&
uri.pathSegments.first == 'details') {
var id = uri.pathSegments[1];
return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
}
return MaterialPageRoute(builder: (context) => UnknownScreen());
},
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('View Details'),
onPressed: () {
Navigator.pushNamed(
context,
'/details/1',
);
},
),
),
);
}
}
class DetailScreen extends StatelessWidget {
String id;
DetailScreen({
this.id,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Viewing details for item $id'),
FlatButton(
child: Text('Pop!'),
onPressed: () {
Navigator.pop(context);
},
),
],
),
),
);
}
}
class UnknownScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text('404!'),
),
);
}
}
这里,settings
是[RouteSettings](https://api.flutter.dev/flutter/widgets/RouteSettings-class.html "RouteSettings")
的一个实例。name和arguments字段是在[Navigator.pushNamed](https://api.flutter.dev/flutter/widgets/Navigator/pushNamed.html "Navigator.pushNamed")
调用时或[initialRoute](https://api.flutter.dev/flutter/material/MaterialApp/initialRoute.html "initialRoute")
设置时提供。
Navigator 2.0
Navigator 2.0 API 在框架中添加了新类,以使APP的页面成为APP state的一个函数,并提供解析来自底层平台的路由(如 Web URL)的能力。以下是新功能的概述:
[**Page**](https://master-api.flutter.dev/flutter/widgets/Page-class.html "**Page**")
— 用于设置导航历史堆栈的不可变对象。[**Router**](https://master-api.flutter.dev/flutter/widgets/Router-class.html "**Router**")
— 用于配置被导航展示的页面列表。通常这个页面列表会根据底层平台或应用程序的状态变化而变化。[**RouteInformationParser**](https://master-api.flutter.dev/flutter/widgets/Router/routeInformationParser.html "**RouteInformationParser**")
,它从[RouteInformationProvider](https://master-api.flutter.dev/flutter/widgets/RouteInformationProvider-class.html "RouteInformationProvider")
接受[RouteInformation](https://master-api.flutter.dev/flutter/widgets/RouteInformation-class.html "RouteInformation")
并将其解析为用户定义的数据类型。[**RouterDelegate**](https://master-api.flutter.dev/flutter/widgets/RouterDelegate-class.html "**RouterDelegate**")
— 定义特定Router
的行为,即如何了解APP状态的变化以及它如何响应这些变化。它的工作是监听RouteInformationParser
和 APP状态并让Navigator
使用当前列表构建Pages
。[**BackButtonDispatcher**](https://master-api.flutter.dev/flutter/widgets/BackButtonDispatcher-class.html "**BackButtonDispatcher**")
— 向Router
报告返回按钮的按下情况。
下图显示了如何RouterDelegate
与交互Router
,RouteInformationParser
以及APP的状态:
以下是这些部分如何相互作用的示例:
- 当平台发出新路由(例如,“books/2”)时,它
RouteInformationParser
会将其转换为T
即在APP中定义的数据类型(例如,名为BooksRoutePath
的类)。 RouterDelegate
的setNewRoutePath
方法使用此数据类型调用,并且必须更新APP状态以更改(例如,通过设置selectedBookId
)并调用notifyListeners.
- 当
notifyListeners
被调用时,它告诉Router
重建RouterDelegate
(使用它的build()
方法) RouterDelegate.build()
返回一个新的Navigator
,其页面根据APP状态作出对应的更改(例如,selectedBookId
)。
导航器 2.0 练习
本节将通过一个例子完成使用 Navigator 2.0 API 的练习。最终会完成一个可以与 URL 栏保持同步的app,并处理来自应用程序和浏览器的后退按钮按下,如下面的 GIF 所示:
接下来,创建一个带有 web 支持的新 Flutter 项目并将其中的内容替换lib/main.dart
为以下内容:
import 'package:flutter/material.dart';
void main() {
runApp(BooksApp());
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class BooksApp extends StatefulWidget {
@override
State<StatefulWidget> createState() => _BooksAppState();
}
class _BooksAppState extends State<BooksApp> {
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Books App',
home: Navigator(
pages: [
MaterialPage(
key: ValueKey('BooksListPage'),
child: Scaffold(),
)
],
onPopPage: (route, result) => route.didPop(result),
),
);
}
}
Pages
Navigator的构造函数有一个新参数pages
。如果Page
对象列表发生变化,则Navigator
会更新路由堆栈。我们通过构建一个显示书籍列表的app来展示它的工作原理。
在 中_BooksAppState
,保持两个状态:书籍列表和选中的书籍:
class _BooksAppState extends State<BooksApp> {
// New:
Book _selectedBook;
bool show404 = false;
List<Book> books = [
Book('Left Hand of Darkness', 'Ursula K. Le Guin'),
Book('Too Like the Lightning', 'Ada Palmer'),
Book('Kindred', 'Octavia E. Butler'),
];
// ...
然后在 中_BooksAppState
,返回Navigator
带有Page
列表的 :
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Books App',
home: Navigator(
pages: [
MaterialPage(
key: ValueKey('BooksListPage'),
child: BooksListScreen(
books: books,
onTapped: _handleBookTapped,
),
),
],
),
);
}
void _handleBookTapped(Book book) {
setState(() {
_selectedBook = book;
});
}
// ...
class BooksListScreen extends StatelessWidget {
final List<Book> books;
final ValueChanged<Book> onTapped;
BooksListScreen({
@required this.books,
@required this.onTapped,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => onTapped(book),
)
],
),
);
}
}
由于此应用程序有两个页面,一个书籍列表和一个显示详细信息的页面,如果选择了一本书(使用collection if
),请显示第二个(详细信息)页面:
pages: [
MaterialPage(
key: ValueKey('BooksListPage'),
child: BooksListScreen(
books: books,
onTapped: _handleBookTapped,
),
),
// New:
if (show404)
MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
else if (_selectedBook != null)
MaterialPage(
key: ValueKey(_selectedBook),
child: BookDetailsScreen(book: _selectedBook))
],
请注意, key
的值是由 book
对象定义的。这告诉Navigator
当Book
对象不同时 MaterialPage
对象与另一个对象是不同的。如果没有唯一的Key,app就无法确定何时在不同的页面之间显示过渡动画。
注意:还可以为Page
自定义行为。例如,为页面添加了一个自定义的过渡动画:
class BookDetailsPage extends Page {
final Book book;
BookDetailsPage({
this.book,
}) : super(key: ValueKey(book));
Route createRoute(BuildContext context) {
return PageRouteBuilder(
settings: this,
pageBuilder: (context, animation, animation2) {
final tween = Tween(begin: Offset(0.0, 1.0), end: Offset.zero);
final curveTween = CurveTween(curve: Curves.easeInOut);
return SlideTransition(
position: animation.drive(curveTween).drive(tween),
child: BookDetailsScreen(
key: ValueKey(book),
book: book,
),
);
},
);
}
}
最后,还需要为pages
参数提供onPopPage
回调。每当Navigator.pop()
调用都会触发此函数。一般用于更新状态(如页面列表),并且必须调用didPop
路由来确定弹出是否成功:
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// Update the list of pages by setting _selectedBook to null
setState(() {
_selectedBook = null;
});
return true;
},
didPop
在更新应用程序状态之前检查是否失败很重要。
使用setState
通知框架调用该build()
方法,该方法在_selectedBook
为 null时返回一个单页列表。
这是完整的示例:
代码语言:javascript复制import 'package:flutter/material.dart';
void main() {
runApp(BooksApp());
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class BooksApp extends StatefulWidget {
@override
State<StatefulWidget> createState() => _BooksAppState();
}
class _BooksAppState extends State<BooksApp> {
Book _selectedBook;
List<Book> books = [
Book('Left Hand of Darkness', 'Ursula K. Le Guin'),
Book('Too Like the Lightning', 'Ada Palmer'),
Book('Kindred', 'Octavia E. Butler'),
];
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Books App',
home: Navigator(
pages: [
MaterialPage(
key: ValueKey('BooksListPage'),
child: BooksListScreen(
books: books,
onTapped: _handleBookTapped,
),
),
if (_selectedBook != null) BookDetailsPage(book: _selectedBook)
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// Update the list of pages by setting _selectedBook to null
setState(() {
_selectedBook = null;
});
return true;
},
),
);
}
void _handleBookTapped(Book book) {
setState(() {
_selectedBook = book;
});
}
}
class BookDetailsPage extends Page {
final Book book;
BookDetailsPage({
this.book,
}) : super(key: ValueKey(book));
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this,
builder: (BuildContext context) {
return BookDetailsScreen(book: book);
},
);
}
}
class BooksListScreen extends StatelessWidget {
final List<Book> books;
final ValueChanged<Book> onTapped;
BooksListScreen({
@required this.books,
@required this.onTapped,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => onTapped(book),
)
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
final Book book;
BookDetailsScreen({
@required this.book,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (book != null) ...[
Text(book.title, style: Theme.of(context).textTheme.headline6),
Text(book.author, style: Theme.of(context).textTheme.subtitle1),
],
],
),
),
);
}
}
就目前而言,此app仅使我们能够以声明方式定义页面堆栈。我们无法处理平台的后退按钮,浏览器的 URL 在我们导航时也不会改变。
Router
到目前为止,该应用程序可以显示不同的页面,但它无法处理来自底层平台的路由,例如,、用户更新浏览器中的 URL。
本节将展示如何实现RouteInformationParser
,RouterDelegate
并更新app的状态。设置后,app会与浏览器的 URL 保持同步。
数据类型
在RouteInformationParser
解析路由信息到用户定义的数据类型,所以我们先定义一个类:
class BookRoutePath {
final int id;
final bool isUnknown;
BookRoutePath.home()
: id = null,
isUnknown = false;
BookRoutePath.details(this.id) : isUnknown = false;
BookRoutePath.unknown()
: id = null,
isUnknown = true;
bool get isHomePage => id == null;
bool get isDetailsPage => id != null;
}
在本app中,所有路由都可以使用一个类来表示。同样也可以选择基础的方式,或以其他方式管理路由信息。
RouterDelegate
接下来,添加一个继承自RouterDelegate
的类:
class BookRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
@override
Widget build(BuildContext context) {
// TODO
throw UnimplementedError();
}
@override
// TODO
GlobalKey<NavigatorState> get navigatorKey => throw UnimplementedError();
@override
Future<void> setNewRoutePath(BookRoutePath configuration) {
// TODO
throw UnimplementedError();
}
}
上面定义的RouterDelegate
的泛型类型是BookRoutePath
,定义了哪些状态显示哪些页面。
我们需要将一些逻辑从_BooksAppState
移到BookRouterDelegate
,并创建一个GlobalKey
. 在此示例中,APP状态直接存储在RouterDelegate
上,也可以分离到另一个类中。
class BookRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
final GlobalKey<NavigatorState> navigatorKey;
Book _selectedBook;
bool show404 = false;
List<Book> books = [
Book('Left Hand of Darkness', 'Ursula K. Le Guin'),
Book('Too Like the Lightning', 'Ada Palmer'),
Book('Kindred', 'Octavia E. Butler'),
];
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
// ...
为了在 URL 中显示正确的路径,我们需要根据App的当前状态返回一个BookRoutePath
:
BookRoutePath get currentConfiguration {
if (show404) {
return BookRoutePath.unknown();
}
return _selectedBook == null
? BookRoutePath.home()
: BookRoutePath.details(books.indexOf(_selectedBook));
}
接下来, 需要在RouterDelegate
中的build
方法返回一个 Navigator
:
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey('BooksListPage'),
child: BooksListScreen(
books: books,
onTapped: _handleBookTapped,
),
),
if (show404)
MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
else if (_selectedBook != null)
BookDetailsPage(book: _selectedBook)
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// Update the list of pages by setting _selectedBook to null
_selectedBook = null;
show404 = false;
notifyListeners();
return true;
},
);
}
在onPopPage
回调中使用notifyListeners
替代setState
,因为此类是一个ChangeNotifier
,而不是一个widget。当RouterDelegate
通知其监听器时,Router
同样会通知RouterDelegate's
的currentConfiguration
已更改并且build
方法需要再次被调用构建新的Navigator
.
该_handleBookTapped
方法也需要使用notifyListeners
代替setState
:
void _handleBookTapped(Book book) {
_selectedBook = book;
notifyListeners();
}
当一个新路由被推送到应用程序时,Router
调用setNewRoutePath
,这使我们的应用程序根据路由的更改更新应用程序状态:
@override
Future<void> setNewRoutePath(BookRoutePath path) async {
if (path.isUnknown) {
_selectedBook = null;
show404 = true;
return;
}
if (path.isDetailsPage) {
if (path.id < 0 || path.id > books.length - 1) {
show404 = true;
return;
}
_selectedBook = books[path.id];
} else {
_selectedBook = null;
}
show404 = false;
}
RouteInformationParser
RouteInformationParser
提供了一个钩子来解析接收的路由信息(RouteInformation
),并将其转换成用户定义的类型(BookRoutePath
)。使用Uri
该类来处理解析:
class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
@override
Future<BookRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
// Handle '/'
if (uri.pathSegments.length == 0) {
return BookRoutePath.home();
}
// Handle '/book/:id'
if (uri.pathSegments.length == 2) {
if (uri.pathSegments[0] != 'book') return BookRoutePath.unknown();
var remaining = uri.pathSegments[1];
var id = int.tryParse(remaining);
if (id == null) return BookRoutePath.unknown();
return BookRoutePath.details(id);
}
// Handle unknown routes
return BookRoutePath.unknown();
}
@override
RouteInformation restoreRouteInformation(BookRoutePath path) {
if (path.isUnknown) {
return RouteInformation(location: '/404');
}
if (path.isHomePage) {
return RouteInformation(location: '/');
}
if (path.isDetailsPage) {
return RouteInformation(location: '/book/${path.id}');
}
return null;
}
}
这个一个定制化的实现,而不是通用路由解析方案。后面会说到更多。
要使用这些新类,我们使用新的MaterialApp.router
构造函数并传入我们的自定义实现:
return MaterialApp.router(
title: 'Books App',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
);
下面是完整的示例:
代码语言:javascript复制import 'package:flutter/material.dart';
void main() {
runApp(BooksApp());
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class BooksApp extends StatefulWidget {
@override
State<StatefulWidget> createState() => _BooksAppState();
}
class _BooksAppState extends State<BooksApp> {
BookRouterDelegate _routerDelegate = BookRouterDelegate();
BookRouteInformationParser _routeInformationParser =
BookRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Books App',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
);
}
}
class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
@override
Future<BookRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
// Handle '/'
if (uri.pathSegments.length == 0) {
return BookRoutePath.home();
}
// Handle '/book/:id'
if (uri.pathSegments.length == 2) {
if (uri.pathSegments[0] != 'book') return BookRoutePath.unknown();
var remaining = uri.pathSegments[1];
var id = int.tryParse(remaining);
if (id == null) return BookRoutePath.unknown();
return BookRoutePath.details(id);
}
// Handle unknown routes
return BookRoutePath.unknown();
}
@override
RouteInformation restoreRouteInformation(BookRoutePath path) {
if (path.isUnknown) {
return RouteInformation(location: '/404');
}
if (path.isHomePage) {
return RouteInformation(location: '/');
}
if (path.isDetailsPage) {
return RouteInformation(location: '/book/${path.id}');
}
return null;
}
}
class BookRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
final GlobalKey<NavigatorState> navigatorKey;
Book _selectedBook;
bool show404 = false;
List<Book> books = [
Book('Left Hand of Darkness', 'Ursula K. Le Guin'),
Book('Too Like the Lightning', 'Ada Palmer'),
Book('Kindred', 'Octavia E. Butler'),
];
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
BookRoutePath get currentConfiguration {
if (show404) {
return BookRoutePath.unknown();
}
return _selectedBook == null
? BookRoutePath.home()
: BookRoutePath.details(books.indexOf(_selectedBook));
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey('BooksListPage'),
child: BooksListScreen(
books: books,
onTapped: _handleBookTapped,
),
),
if (show404)
MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
else if (_selectedBook != null)
BookDetailsPage(book: _selectedBook)
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// Update the list of pages by setting _selectedBook to null
_selectedBook = null;
show404 = false;
notifyListeners();
return true;
},
);
}
@override
Future<void> setNewRoutePath(BookRoutePath path) async {
if (path.isUnknown) {
_selectedBook = null;
show404 = true;
return;
}
if (path.isDetailsPage) {
if (path.id < 0 || path.id > books.length - 1) {
show404 = true;
return;
}
_selectedBook = books[path.id];
} else {
_selectedBook = null;
}
show404 = false;
}
void _handleBookTapped(Book book) {
_selectedBook = book;
notifyListeners();
}
}
class BookDetailsPage extends Page {
final Book book;
BookDetailsPage({
this.book,
}) : super(key: ValueKey(book));
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this,
builder: (BuildContext context) {
return BookDetailsScreen(book: book);
},
);
}
}
class BookRoutePath {
final int id;
final bool isUnknown;
BookRoutePath.home()
: id = null,
isUnknown = false;
BookRoutePath.details(this.id) : isUnknown = false;
BookRoutePath.unknown()
: id = null,
isUnknown = true;
bool get isHomePage => id == null;
bool get isDetailsPage => id != null;
}
class BooksListScreen extends StatelessWidget {
final List<Book> books;
final ValueChanged<Book> onTapped;
BooksListScreen({
@required this.books,
@required this.onTapped,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => onTapped(book),
)
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
final Book book;
BookDetailsScreen({
@required this.book,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (book != null) ...[
Text(book.title, style: Theme.of(context).textTheme.headline6),
Text(book.author, style: Theme.of(context).textTheme.subtitle1),
],
],
),
),
);
}
}
class UnknownScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text('404!'),
),
);
}
}
现在可以在chrome中试试效果了。
TransitionDelegate
通过TransitionDelegate
可以自定义页面过度动画。如果您需要对此进行自定义,请继续阅读,但如果您对默认行为感到满意,则可以跳过此部分。
使用方法如下:
代码语言:javascript复制// New:
TransitionDelegate transitionDelegate = NoAnimationTransitionDelegate();
child: Navigator(
key: navigatorKey,
// New:
transitionDelegate: transitionDelegate,
例如,以下实现禁用所有过渡动画:
代码语言:javascript复制class NoAnimationTransitionDelegate extends TransitionDelegate<void> {
@override
Iterable<RouteTransitionRecord> resolve({
List<RouteTransitionRecord> newPageRouteHistory,
Map<RouteTransitionRecord, RouteTransitionRecord>
locationToExitingPageRoute,
Map<RouteTransitionRecord, List<RouteTransitionRecord>>
pageRouteToPagelessRoutes,
}) {
final results = <RouteTransitionRecord>[];
for (final pageRoute in newPageRouteHistory) {
if (pageRoute.isWaitingForEnteringDecision) {
pageRoute.markForAdd();
}
results.add(pageRoute);
}
for (final exitingPageRoute in locationToExitingPageRoute.values) {
if (exitingPageRoute.isWaitingForExitingDecision) {
exitingPageRoute.markForRemove();
final pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
if (pagelessRoutes != null) {
for (final pagelessRoute in pagelessRoutes) {
pagelessRoute.markForRemove();
}
}
}
results.add(exitingPageRoute);
}
return results;
}
}
这个自定义实现覆盖了resolve()
,它负责将各种路由如推送、弹出、添加、完成或删除:
markForPush
— 显示带有动画过渡的路线markForAdd
— 显示_没有_动画过渡的路线markForPop
— 移除带有动画过渡的路线并用结果完成它。在这种情况下,“完成”意味着result
对象被传递到 上的onPopPage
回调AppRouterDelegate
。markForComplete
— 删除没有过渡的路线并用一个完成它result
markForRemove
— 删除没有动画过渡且未完成的路线。
这个类仅影响_声明式_API,这就是后退按钮仍显示过渡动画的原因。
参考资料
[1]
设计文档: https://flutter.dev/docs/resources/design-docs
[2]
Navigator 2.0 和 Router 的: https://docs.google.com/document/d/1Q0jx0l4-xymph9O6zLaOY4d_f7YFpNWX_eGbzYxr9wY/edit#heading=h.l6kdsrb6j9id
[3]
_声明性_API: https://flutter.dev/docs/get-started/flutter-for/declarative
[4]
很难推送或弹出多个页面: https://github.com/flutter/flutter/issues/12146
[5]
将参数传递给命名的路由: https://flutter.dev/docs/cookbook/navigation/navigate-with-arguments
-------------都到这了,点一下关注再走---------