如何理解 Flutter 路由源码设计?| 开发者说·DTalk
本文原作者: Nayuta,原文发布于: 进击的 Flutter
本期看点:
70 行代码实现一个丐版路由
路由源码细节解析
导语
某天在公众号看到这样一个问题
这问题我熟啊,刚好翻译 OverlayEntries 和 Routes 进行了重建优化里面提到,在 1.17 版本之后,当我们打开一个新页面 (Route),前一个页面将不再重新构建。
啪,我直接一个链接甩出去,潇洒退场。
过了一会儿我再瞅
(尴尬而不失礼貌的微笑) 所以为了搞清楚前一个页面为什么 build,我基于 1.12.13 版本写了个 demo 测试,结果发现不止是前一个页面会再次 build,前面所有的页面都会 build。
按常理上一页面被覆盖了就不应该再次构建了!这是发生了什么?要搞清这个问题可还真不那么容易,划分两期来分析原理。本篇会和大家从源码分析一个最熟悉的陌生人路由。
一、初识路由: 一种页面切换的能力
为什么先分析路由,因为问题发生在页面切换的场景下。
Flutter 中我们往往通过这样一行代码
打开到一个新的页面 PageE,调用 Navigator.of(context).pop() 退出一个页面。所以路由简单来说,就是一种页面切换的能力。
Flutter 如何实现这一能力?为了更深刻理解源码设计,本期我们换个思路,让我们抛开现在的路由机制思考: 假如 framework 移除了路由机制,您会如何实现页面切换?
二、如何实现一个丐版路由
1、设计路由容器
为了管理每个页面的退出和进入,我们可以设计一个路由容器进行管理,那这个容器该如何设计?观察页面打开和关闭这两个过程,其实非常简单。打开就是目标页面覆盖了上一个页面,而退出过程则刚好相反。
根据系统现有的 Widget 我们很自然想到了 Stack,Stack 类似原生的相对布局,每个 Widget 可以根据自己的位置叠加显示在屏幕上。只要我们把它的每个子 widget 都撑满,那么 Stack 每次只会显示最后一个 widget,这不就类似每次打开一个页面么。
class RouteHostState extends State<RouteHost> with TickerProviderStateMixin {List<Widget> pages = []; //路由中的多个页面@overrideWidget build(BuildContext context) {return Stack(fit: StackFit.expand, //每个页面撑满屏幕children: pages,);}
}
2、提供页面切换方法
因为容器基于 Stack 所以打开和关闭页面也非常简单。对于打开一个页面我们只需要将新的页面添加到 pages 中;关闭页面,我们只要移除最后一个即可。为了让切换过程更加流畅,可以添加一些动画转场效果。
以打开页面为例其实只需三步
Step 1、创建一个转场动画
//1、创建一个位移动画AnimationController animationController;Animation<Offset> animation;animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 500));animation = Tween(begin: Offset(1, 0), end: Offset.zero).animate(animationController);
Step 2、将目标页面添加到 stack 中显示
//2、添加到 stack 中并显示pages.add(SlideTransition(position: animation,child: page,));
Step 3、开启转场动画
//3、调用 setState 并开启转场动画setState(() {animationController.forward();}
是的,简单来说只需要这三步即可完成,我们可以看看效果
关闭页面则反过来即可。
//关闭最后一个页面void close() async {//出场动画await controllers.last.reverse();//移除最后一个页面pages.removeLast();controllers.removeLast().dispose();}
}
3、让子页面使用路由能力
上面我们提到打开和关闭页面方法都在路由容器中,那子页面如何能使用这个能力?这个问题背后其实是 Flutter 中一个很有意思的话题,父子节点如何数据传递?
我们知道 Flutter 框架体系中有三棵树,在《Widget、Element、Render 是如何形成树结构?》中熟悉了它们的构建过程。Flutter 提供了多个方法让我们可以访问父子节点:
abstract class BuildContext {///查找父节点中的T类型的StateT findAncestorStateOfType<T extends State>();///查找父节点中的T类型的 InheritedWidget 例如 MediaQuery 等T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object aspect })///遍历子元素的element对象void visitChildElements(ElementVisitor visitor);......
}
源码中例如我们常使用的 Navigator、MediaQuery、InheritedTheme,以及很多状态管理框架也是基于这个原理实现。同样的,可以通过这样的方法将路由能力提供给子页面。
///RouteHost提供给子节点访问自己 State 的能力static RouteHostState of(BuildContext context) {return context.findAncestorStateOfType<RouteHostState>();}///子节点借助上面的方法使用路由void openPage() {RouteHost.of(context).open(RoutePage());}
完整案例可以查看:
https://github.com/Nayuta403/flutter_lifecycle_example
三、理解路由源码设计
有了上面的思考,那么对于源码的设计我们就很清晰了。现在我们回过头来看看路由的使用
Navigator.of(context).push(MaterialPageRoute(builder: (c) {return PageB();}));
对比我们设计的路由,来拆解原理。
RouteHost.of(context).open(RoutePage());
路由容器: Navigator
对比两个方法,其实我们就明白了 Navigator 就是起到路由容器的作用。查看源码您会发现,他被嵌套在 MaterialApp 中,并且 Nagivator 内部也是通过 Stack 实现。
我们的每一个页面都是 Navigator 的子节点,自然可以通过 context 去获取它。
static NavigatorState of(BuildContext context) {///获取位于根部的 Navigatorfinal NavigatorState navigator = rootNavigator? context.findRootAncestorStateOfType<NavigatorState>(): context.findAncestorStateOfType<NavigatorState>();return navigator;}
Route: 处理页面转场等设计
明白了 Navigator 之后,我们发现每次打开页面的时候往往需要传入 PageRoute 对象,这又起到什么作用呢?
在我们上面的设计中,为了让过渡自然,我们在 open 方法中,手动的为每一个页面添加了转场动画。而 Flutter 中将路由切换所需的动画,交互阻断等封装成了 Route 对象。通过层次封装的形式,逐层实现了这些能力:
有了前面的思考之后,再看路由源码的设计,思路其实变得非常清晰。对于源码的学习,千万不要一开始深陷在细节中,从整体思考再拆解流程,这样方可深入浅出。
四、源码中的亿点点细节
有了整体大框架之后,我们可以具体梳理 Navigator.of(context).push 过程。
Future<T> push<T extends Object>(Route<T> route) {final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;/// 1、新页面的路由进行添加route._navigator = this;route.install(_currentOverlayEntry); ///关键方法!!!!!!!_history.add(route);route.didPush();route.didChangeNext(null);/// 2、上一个路由的相关回调if (oldRoute != null) {oldRoute.didChangeNext(route);route.didChangePrevious(oldRoute);}/// 3、回调 Navigator 的观察者for (NavigatorObserver observer in widget.observers)observer.didPush(route, oldRoute);RouteNotificationMessages.maybeNotifyRouteChange(_routePushedMethod, route, oldRoute);_afterNavigation(route);return route.popped;}
这里我们只需关注核心的第一个过程,关键方法在:
route.install(_currentOverlayEntry);
这个方法被 Route 的子类重写,并且分层完成了不同逻辑:
在 OverlayRoute 中如下:
void install(OverlayEntry insertionPoint) {/// 通过 createOverlayEntries() 创建新页面的 _overlayEntries 集合/// 这个 _overlayEntries 集合就是我们打开的新页面_overlayEntries.addAll(createOverlayEntries());/// 将新页面的 _overlayEntries 集合插入到 overlay 中显示navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);super.install(insertionPoint);}Iterable<OverlayEntry> createOverlayEntries() sync* {/// 创建一个遮罩yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);/// 创建页面实际内容,最终调用到 Route 的 builder 方法yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);}
第一行代码中的 createOverlayEntries() 方法会先创建一个 zhe 调用到 Route 的 builder 方法,创建我们需要打开的页面与遮罩,之后将整个集合添加到 Overlay 中 (如果不太熟悉 Overlay 将它当做一个 Stack 就行)。
/// overlay.dart
void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry below, OverlayEntry above }) {setState(() {_entries.insertAll(_insertionIndex(below, above), entries);});}
overlay 的方法也很简单,添加页面到 _entries 调用 setState() 更新。这个 _entries 简单来看就和我们前面设计的 pages 类似,不过里面多了选择渲染的能力,我们下一期再详细分析。
五、总结
看到这,相信您对于 Flutter 中的路由再也不会感到陌生,总结下来关键有三点:
Navigator 作为路由容器内部嵌套了 Stack 提供了页面切换的能力;
通过 context.findRootAncestorStateOfType() 可以访问父节点;
Route 为我们封装了切换时需要的其他能力。
当我们切换页面的时候,上一个页面默认会走以下几个生命周期:
这又是为什么?一定是这样的顺序么?Flutter 生命周期到底该怎么回答?我们留着下一期再分析啦~
长按右侧二维码
查看更多开发者精彩分享
"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。
点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"
如何理解 Flutter 路由源码设计?| 开发者说·DTalk相关推荐
- Flutter 路由源码解析
前言 这一次,我尝试以不贴一行源代码的方式向你介绍 Flutter 路由的实现原理,同时为了提高你阅读源码的积极性,除了原理介绍以外,又补充了两个新的模块:从源码中学习到的编程技巧,以及 阅读源码之后 ...
- ThinkPHP路由源码解析(三)
本文接着上文继续来解读路由源码,如果你看到本文可以先看一下之前写的路由文章,共计俩篇. ThinkPHP路由源码解析 前言 一.检测路由-合并分组参数.检查分组路由 二.检测URL变量和规则路由是否匹 ...
- ensp大型网络环境设计与实现_mongodb内核源码设计实现、性能优化、最佳运维系列-网络传输层模块源码实现三...
1. 说明 在之前的<<Mongodb网络传输处理源码实现及性能调优-体验内核性能极致设计>>和<<mongodb内核源码设计实现.性能优化.最佳运维系列-tran ...
- 【Flutter】Flutter 拍照示例 ( Flutter 插件配置 | Flutter 插件源码示例 | iOS 应用配置 | Android 应用配置 )
文章目录 一.Flutter 插件配置 二.Flutter 插件源码示例 三.iOS 应用配置 四.Android 应用配置 五.相关资源 一.Flutter 插件配置 Flutter 拍照示例中 , ...
- Deep Compression阅读理解及Caffe源码修改
Deep Compression阅读理解及Caffe源码修改 作者:may0324 更新: 没想到这篇文章写出后有这么多人关注和索要源码,有点受宠若惊.说来惭愧,这个工作当时做的很粗糙,源码修改的比 ...
- 深入理解 AuthenticationManagerBuilder 【源码篇】
咱们继续来撸 Spring Security 源码. 前面和大家分享了 SecurityBuilder 以及它的一个重要实现 HttpSecurity,在 SecurityBuilder 的实现类里边 ...
- 优秀的GPS定位系统源码对开发者意味着什么
一套优秀的GPS定位系统源码对开发者意味着什么? 其实这就好比你一辆汽车行驶在高速公路上和行驶在坑坑洼洼泥泞道路上的区别:一个是可以让你高速行驶更快更顺利的到底终点:另一个不仅让你连平时该跑的速度都达 ...
- 深入理解ceph-disk activate 源码逻辑
文章目录 CEPH-DISK代码逻辑 `Activate osd`的主要逻辑如下 DEF main_activate激活osd的入口函数 DEF mount_activate挂载临时目录,分配osd ...
- Android源码设计模式分析项目
原文链接:https://github.com/simple-android-framework/android_design_patterns_analysis Android源码设计模式分析开源项 ...
最新文章
- R语言使用unzip函数解压压缩文件(Extract or List Zip Archives)
- PHPCMS V9 框架代码分析(入口程序)
- python_day6.2
- matlab小波变换边缘检测,在matlab 下 实现 用小波变换对图像进行边缘检测 程序代码...
- python 程序运行插件_如何使Python插件在Pluma中运行?
- Android系统(31)--- 如何分析native memory leak
- 8086汇编语言实现数组冒泡排序(全注释)
- 售货员的难题(codevs 2596)
- Pthreads线程的基本常识
- Android自定义控件7--自定义开关--绘制界面内容
- 用hudson配置持续集成CI服务器几个关键的配置
- 【SQL Server】入门教程(总结篇)
- 谷歌出品!机器学习常用术语总结
- 《仿人机器人原理与实战》一第2章
- Balanced Multimodal Learning via On-the-fly Gradient Modulation论文笔记
- Failed to convert value of type 'java.lang.String' to required type 'java.util.Date
- web 前端签名插件_signature_pad插件实现电子签名功能
- 社区o2o怎么做线上推广?
- 关于Coursera
- 移动银行的技术、业务和商业模式
热门文章
- SD-WAN专线在智能物流方面的发展与应用
- 关键字_restrict
- 能够出线的学生序号(0~9),每行一个序号。
- mactype支持qq浏览器
- HyperLogLog--统计用户访问量
- html中table的colspan,表格中的colspan colspan
- Unity3D学习笔记(一)界面介绍
- JMeter工具:常用协议脚本开发(BeanShell Sampler, Debug Sampler, FTP/Java/JDBC请求, JUnit request, SOAP/XML-RPC)
- 最赚钱的行业和公司排行榜(verified 版本)
- 基于c#的串口设备通讯c#项目工程含虚拟串口软件与串口通信工具(C#源码)