译学习Flutter中新的Navigator和Router系统
Posted 唯鹿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了译学习Flutter中新的Navigator和Router系统相关的知识,希望对你有一定的参考价值。
原文:Learning Flutter’s new navigation and routing system
作者:John Ryan
本文解释了Flutter新的Navigator
和Router
API是如何工作。如果你关注Flutter的开放设计文档,你可能已经看到这些新功能被称为Navigator 2.0和Router。我们将探讨这些API如何实现对应用程序中的屏幕进行更精细的控制,以及如何使用它来解析路由。
这些新的API并不是破坏性的更改,它只是增加了一个新的声明式API。在Navigator 2.0之前,很难push或pop多个页面,或者删除当前页面下面一个页面。如果你对Navigator
目前的工作方式很满意,你可以继续以同样的方式(命令式的)使用它。
Router
提供了处理来自底层平台的路由并显示相应页面的能力。在本文中,Router
被配置为解析浏览器URL以显示相应的页面。
本文将帮助您选择哪种 Navigator
模式最适合您的应用程序,并解释了如何使用 Navigator 2.0 来解析浏览器 URL,并完全控制活动页面的堆栈。本文的例子展示了如何构建一个应用程序,以处理来自平台的传入路由并管理应用程序的页面。下面的GIF展示了这个示例程序的操作:
Navigator 1.0
如果你正在使用Flutter,那么您可能正在使用Navigator
并熟悉以下概念:
在Navigator 2.0之前,Route
通过命名路由或匿名路由push和pop到Navigator
的堆栈中。接下来的部分将简要回顾这两种方法。
匿名路由
大多数移动应用的页面都是彼此叠放在一起,就像堆栈一样。在Flutter中,使用Navigator
很容易做到这一点。
MaterialApp
和CupertinoApp
已经使用Navigator
。您可以使用Navigator.of()
访问它,也可以使用Navigator.push()
显示一个新页面,并使用Navigator.pop()
返回上一个页面。
mport '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树的一部分,所以当DetailScreen
可见时,与它相关的任何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);
,
),
),
);
这些路由必须是预先定义的。虽然您可以向命名路由传递参数,但不能从路由本身解析参数。例如,如果应用程序在Web上运行,你就不能从/details/:id
这样的路由中解析ID。
使用onGenerateRoute的高级命名路由
处理命名路由更灵活的方法是使用onGenerateRoute
。这个API让你有能力处理所有的路径,这下面是完整的示例:
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的一个实例。name和arguments字段是调用Navigator.pushNamed时提供的值,或者是initialRoute设置的值。
Navigator 2.0
Navigator 2.0 API为框架增加了新的类,以便使应用的屏幕成为应用状态的函数,并从底层提供解析路由的能力(如Web URL)。下面是对新内容的概述:
- Page — 一个不可更改的对象,用于设置Navigator的历史堆栈。
- Router — 配置要由Navigator显示的页面列表。通常此页面列表根据平台或应用的状态变化而变化。
- RouteInformationParser,它从RouteInformationProvider中获取RouteInformation,并将其解析为用户定义的数据类型。
- RouterDelegate — 定义了
Router
如何学习应用状态变化以及如何响应这些变化的应用特定行为。它的工作是监听RouteInformationParser
和应用状态,并利用当前的Pages
列表构建Navigator
。 - BackButtonDispatcher — 向
Router
报告返回按钮按下的情况。
下图显示了RouterDelegate
如何与Router
、RouteInformationParser
和应用的状态进行交互。
下面是这些部件如何交互的一个例子:
- 当平台发出一个新的路由(例如,“books/2”)时,
RouteInformationParser
将其转换为你在应用中定义的抽象数据类型T
(例如名为BooksRoutePath
的类)。 RouterDelegate
的setNewRoutePath
方法是用这个数据类型调用的,必须更新应用程序状态以反映变化(例如,通过设置selectedBookId
),并调用notifyListeners
。- 当
notifyListeners
被调用时,它告诉Router
重建RouterDelegate
(使用其build()
方法)。 RouterDelegate.build()
返回一个新的Navigator
,其页面现在反映了应用状态的变化(例如selectedBookId
)。
Navigator 2.0 练习
本节将带领你完成一个使用Navigator 2.0 API的练习。我们最终会得到一个可以与URL栏保持同步的应用,并处理来自应用和浏览器的返回按钮操作,如下图所示。
切换到master渠道,创建一个支持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
就会更新堆栈的路由来匹配。为了了解其工作原理,我们将构建一个显示书籍列表的应用程序。
在_BooksAppState
中,保留两种状态:书籍列表和所选书籍。
class _BooksAppState extends State<BooksApp>
// New:
Book _selectedBook;
bool show404 = false;
List<Book> books = [
Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];
// ...
然后在_BooksAppState
中,返回一个带有Page
对象列表的Navigator
。
@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
,框架就无法决定何时在不同的Pages
之间显示过渡动画。
注意:如果你喜欢,你也可以扩展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
来确定pop是否成功。
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
为空时,该方法返回一个带有单页的列表。
下面是完整的例子。
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('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];
@override
Widget build(BuildContext context)
return MaterialApp(
title: 'Books App',
home: Navigator(
pages: [
Materia以上是关于译学习Flutter中新的Navigator和Router系统的主要内容,如果未能解决你的问题,请参考以下文章
Flutter 一起使用 navigator 和 ChangeNotifierProvider 的最佳方法是啥