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!’),
),
);
}
}
这里,我们可以通过 RouteSettings
类型的对象 settings
可以拿到 Navigator.pushNamed
调用时传入的参数。
Navigator 2.0
Navigator 2.0 提供了一系列全新的接口,可以实现将路由状态成为应用状态的一部分,并能够通过底层 API 实现参数解析的功能,新增的 API 如下:
-
Page,用来表示
Navigator
路由栈中各个页面的配置信息。 -
Router,用来制定要由
Navigator
展示的页面列表,通常,该页面列表会根据系统或应用程序的状态改变而改变。 -
RouteInformationParser,持有
RouteInformationProvider
提供的RouteInformation
,可以将其解析为我们定义的数据类型。 -
RouterDelegate,定义应用程序中的路由行为,例如 Router 如何知道应用程序状态的变化以及如何响应。主要的工作就是监听
RouteInformationParser
和应用状态并通过当前页面列表构建 ·。 -
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 做一个小小练习,我们将实现一个 Flutter 应用,该应用作用在 Web 上时路由状态会与浏览器中的 URL 连接保持一致,而且也能够处理浏览器的回退按钮,如下:
接下来,使用 flutter channel master
将 Flutter 切换到 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 createState() => _BooksAppState();
}
class _BooksAppState extends State {
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 {
// New:
Book _selectedBook;
bool show404 = false;
List 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 books;
final ValueChanged 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 对象中的值定义作为 MaterialPage
的唯一标识,也就是说,book 对象不同这里的 MaterialPage
就不同。没有唯一的 key,框架就无法确定何时显示不同 Page 之间的过渡动画。
我们还可以继承 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()
被调用时就会出发这个函数,我们可以在其中更新路由状态
最后,在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;
},
我们还必须在更新应用程序状态之前检查是否 pop 失败。这里,我们使用了 setState
方法来通知 Flutter 调用 build()
方法,该方法 _selectedBook
为 null 表示展示书单列表页。
完整代码如下:
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 createState() => _BooksAppState();
}
class _BooksAppState extends State {
Book _selectedBook;
List 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 books;
final ValueChanged 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),
],
],
),
),
);
}
}
目前,我们就实现了声明式的路由管理,单此时我们还无法处理浏览器的后退按钮,也不能同步浏览器地址拦中的链接。
Router
本节,我们来实现通过 RouteInformationParser
, RouterDelegate
更新路由状态,实现与浏览器地址拦中的链接同步
数据类型
首先,我们需要通过 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;
}
在该应用程序中,可以使用 BookRoutePath
类来表示应用程序中的路由路径,我们也可以实现父子类来关系其他各类型的路由信息。
RouterDelegate
接下来,我们实现一个 RouterDelegate
的子类 BookRouterDelegate
:
class BookRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
@override
Widget build(BuildContext context) {
// TODO
throw UnimplementedError();
}
@override
// TODO
GlobalKey get navigatorKey => throw UnimplementedError();
@override
Future setNewRoutePath(BookRoutePath configuration) {
// TODO
throw UnimplementedError();
}
}
BookRouterDelegate
的泛型为 BookRoutePath
,其中包含了决定显示哪个页面所需的所有状态。
此时,我们就可以将 _BooksAppState
中的路由相关的逻辑放到 BookRouterDelegate
中,这里,我们创建了一个 GlobalKey
对象,其他各个状态也都保存在这里面:
class BookRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
final GlobalKey navigatorKey;
Book _selectedBook;
bool show404 = false;
List books = [
Book(‘Stranger in a Strange Land’, ‘Robert A. Heinlein’),
Book(‘Foundation’, ‘Isaac Asimov’),
Book(‘Fahrenheit 451’, ‘Ray Bradbury’),
];
BookRouterDelegate() : navigatorKey = GlobalKey();
// …
为了能在 URL 中显示正确的路径,我们也需要根据应用程序的当前状态返回一个 BookRoutePath
对象:
BookRoutePath get currentConfiguration {
if (show404) {
return BookRoutePath.unknown();
}
return _selectedBook == null
-
? BookRoutePath.home()
- BookRoutePath.details(books.indexOf(_selectedBook));
}
下面,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;
},
);
}
因为该类并不是组件,而是由 ChangeNotifier
实现,因此这里的 onPopPage
方法需要使用 notifyListeners
替代 setState
来改变状态,当 RouterDelegate
触发状态更新时,Router
同样会触发 RouterDelegate
的 currentConfiguration
方法并调用 build
方法创建出一个新的 Navigator
组件。
_handleBookTapped
方法也需要使用 notifyListeners
代替 setState
:
void _handleBookTapped(Book book) {
_selectedBook = book;
notifyListeners();
}
新页面打开后,Router
会调用setNewRoutePath
方法来更新应用程序的路由状态:
@override
Future 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 {
@override
Future 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 createState() => _BooksAppState();
}
class _BooksAppState extends State {
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 {
@override
Future 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
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
final GlobalKey navigatorKey;
Book _selectedBook;
bool show404 = false;
List books = [
Book(‘Stranger in a Strange Land’, ‘Robert A. Heinlein’),
Book(‘Foundation’, ‘Isaac Asimov’),
Book(‘Fahrenheit 451’, ‘Ray Bradbury’),
];
BookRouterDelegate() : navigatorKey = GlobalKey();
BookRoutePath get currentConfiguration {
if (show404) {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/2270d6d3d13a5f3478603889785176a8.jpeg)
总结
最后小编想说:不论以后选择什么方向发展,目前重要的是把Android方面的技术学好,毕竟其实对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!
这里附上我整理的几十套腾讯、字节跳动,京东,小米,头条、阿里、美团等公司19年的Android面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
技术进阶之路很漫长,一起共勉吧~
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
bool show404 = false;
List books = [
Book(‘Stranger in a Strange Land’, ‘Robert A. Heinlein’),
Book(‘Foundation’, ‘Isaac Asimov’),
Book(‘Fahrenheit 451’, ‘Ray Bradbury’),
];
BookRouterDelegate() : navigatorKey = GlobalKey();
BookRoutePath get currentConfiguration {
if (show404) {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-JyM7sDJU-1712615382758)]
[外链图片转存中…(img-3FBuMk72-1712615382759)]
[外链图片转存中…(img-cenRip9z-1712615382760)]
[外链图片转存中…(img-gyxBgP63-1712615382760)]
[外链图片转存中…(img-L9CsZ2xT-1712615382760)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/2270d6d3d13a5f3478603889785176a8.jpeg)
总结
最后小编想说:不论以后选择什么方向发展,目前重要的是把Android方面的技术学好,毕竟其实对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!
这里附上我整理的几十套腾讯、字节跳动,京东,小米,头条、阿里、美团等公司19年的Android面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
[外链图片转存中…(img-hHZ6RdM9-1712615382761)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
技术进阶之路很漫长,一起共勉吧~
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!