赞
踩
原文: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展示了这个示例程序的操作:
如果你正在使用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
。这个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 API为框架增加了新的类,以便使应用的屏幕成为应用状态的函数,并从底层提供解析路由的能力(如Web URL)。下面是对新内容的概述:
Router
如何学习应用状态变化以及如何响应这些变化的应用特定行为。它的工作是监听RouteInformationParser
和应用状态,并利用当前的Pages
列表构建Navigator
。Router
报告返回按钮按下的情况。下图显示了RouterDelegate
如何与Router
、RouteInformationParser
和应用的状态进行交互。
下面是这些部件如何交互的一个例子:
RouteInformationParser
将其转换为你在应用中定义的抽象数据类型T
(例如名为BooksRoutePath
的类)。RouterDelegate
的setNewRoutePath
方法是用这个数据类型调用的,必须更新应用程序状态以反映变化(例如,通过设置selectedBookId
),并调用notifyListeners
。notifyListeners
被调用时,它告诉Router
重建RouterDelegate
(使用其build()
方法)。RouterDelegate.build()
返回一个新的Navigator
,其页面现在反映了应用状态的变化(例如selectedBookId
)。本节将带领你完成一个使用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), ), ); } }
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: [ 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), ], ], ), ), ); } }
就目前而言,这个应用只能让我们以声明式定义页面的堆栈。我们无法处理平台的后退按钮,浏览器的URL也不会随着我们的Navigator
而改变。
到目前为止,该应用可以显示不同的页面,但不能处理来自底层平台的路由,比如用户更新浏览器中的URL。
本节介绍如何实现RouteInformationParser
、RouterDelegate
以及更新应用状态。一旦设置好,应用程序就会与浏览器的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; }
在这个应用中,应用中所有的路由都可以使用一个类来表示。相反,你也可以选择使用不同的类来实现一个超类,或者用另一种方式来管理路由信息。
接下来,添加一个扩展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
。在这个例子中,应用状态直接存储在RouterDelegate
上,但也可以分离到另一个类中。
class BookRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
final GlobalKey<NavigatorState> navigatorKey;
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'),
];
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
// ...
为了在URL中显示正确的路径,我们需要根据应用的当前状态返回一个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
widget 。 RouterDelegate
的 currentConfiguration
已经改变,需要再次调用其build
方法来构建新的 Navigator
。
_handleBookTapped
方法也需要使用notifyListeners
而不是setState
。
void _handleBookTapped(Book book) {
_selectedBook = book;
notifyListeners();
}
当一个新的路由被push时,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
提供了一个钩子来解析传入的路由(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,
);
这是完整的示例:
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('Stranger in a Strange Land', 'Robert A. Heinlein'), Book('Foundation', 'Isaac Asimov'), Book('Fahrenheit 451', 'Ray Bradbury'), ]; 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浏览器中运行这个示例,现在可以显示正在导航的路由,并在手动编辑URL时导航到正确的页面。
您可以提供TransitionDelegate
的自定义实现,自定义当页面列表发生变化时,路由如何出现在(或从)屏幕上。如果您需要自定义,请继续阅读,但如果您对默认行为满意,可以跳过本节。
为导航器提供一个自定义的TransitionDelegate
,定义所需的行为:
// New:
TransitionDelegate transitionDelegate = NoAnimationTransitionDelegate();
child: Navigator(
key: navigatorKey,
// New:
transitionDelegate: transitionDelegate,
例如,下面的实现会禁用所有的过渡动画。
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
完成它。在此上下文中,"完成 "意味着result
对象被传递给AppRouterDelegate
的onPopPage
回调。markForComplete
- 删除路由时没有动画过渡,并用result
来完成它。markForRemove
- 删除路由时没有动画过渡,且没有完成。这个类只影响声明式API,这就是为什么点击后退按钮仍然显示过渡动画。
本示例的工作方式:这个例子既关注新的路由,也关注退出屏幕的路由。它遍历newPageRouteHistory
中的所有对象,并使用markForAdd
将它们标记为不需要过渡动画的添加。接下来,它遍历locationToExitingPageRoute
map 的值。如果它找到一个标记为isWaitingForExitingDecision
的路由,那么它就调用markForRemove
来表示该路由应该在没有过渡和没有完成的情况下被删除。
这个较大的演示程序展示了如何在一个Router
中添加另一个Router
。许多应用程序需要为BottomAppBar
中的目的地提供路由,并为其上方的一堆视图提供路由,这需要两个导航器。为此,应用使用状态对象来存储应用特定的导航状态(选定的菜单索引和选定的Book
对象)。这个例子还展示了如何配置哪个Router
处理后退按钮。
本文探讨了如何在特定的应用中使用这些API,但也可以用来构建更高级别的API包。我们希望你能和我们一起探讨在这些功能之上构建的更高级的API能为用户做什么。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。