flutter 路由机制的实现
整个 flutter 应用的运行都只是基于原生应用中的一个 view,比如 android 中的 flutterview,flutter 中的页面切换依赖于它的路由机制,也就是以 navigator 为中心的一套路由功能,使得它能够完成与原生类似且能够自定义的页面切换效果。
下面将介绍 flutter 中的路由实现原理,包括初始化时的页面加载、切换页面的底层机制等。
实现基础
flutter 应用的运行需要依赖 materialapp/cupertinoapp 这两个 widget,他们分别对应着 android/ios 的设计风格,同时也为应用的运行提供了一些基本的设施,比如与路由相关的主页面、路由表等,再比如跟整体页面展示相关的 theme、locale 等。
其中与路由相关的几项配置有 home、routes、initialroute、ongenerateroute、onunknownroute,它们分别对应着主页面 widget、路由表(根据路由找到对应 widget)、首次加载时的路由、路由生成器、未知路由代理(比如常见的 404 页面)。
materialapp/cupertinoapp 的子结点都是 widgetsapp,只不过他们给 widgetsapp 传入了不同的参数,从而使得两种 widget 的界面风格不一致。navigator 就是在 widgetsapp 中创建的,
在 widgetsapp 的 build 中第一个创建的就是 navigator,主要看一下它的参数,首先,_navigator 是一个 globalkey,使得 widgetsapp 可以通过 key 调用 navigator 的函数进行路由切换,也就是在 widgetsbinding 中处理 native 的路由切换信息的时候,最终是由 widgetsapp 完成的。另外这里的 _navigator 应该只在 widgetsapp 中有使用,其他地方需要使用一般是直接调用 navigator.of 获取,这个函数会沿着 element 树向上查找到 navigatorstate,所以在应用中切换路由是需要被 navigator 包裹的,不过由于 widgetsapp 中都有生成 navigator,开发中也不必考虑这些。
另外,就是关于底层获取上层 navigatorelement 实例的方式,在 element 树中有两种方式可以从底层获取到上层的实例,一种方式是使用 inheritedwidget,另一种就是直接沿着树向上查找(ancestorxxxofexacttype 系列),两种方式的原理基本是一致的,只不过 inheritedwidget 在建立树的过程中会一层层向下传递,而后者是使用的时候才向上查找,所以从这个角度来说使用 inheritedwidget 会高效些,但是 inheritedwidget 的优势不止如此,它是能够在数据发生改变的时候通知所有依赖它的结点进行更新,这也是 ancestorxxxofexacttype 系列所没有的。
然后 initialroute 规定了初始化时候的页面,由 widgetsbinding.instance.window.defaultroutename 和 widget.initialroute 来决定,不过前者优先级更高,因为这个是 native 中指定的,以 android 为例,在启动 flutteractivity 的时候可以传入 route 字段指定初始化页面。
ongenerateroute 和 onunknownroute 是获取 route 的策略,当 ongenerateroute 没有命中时会调用 onunknownroute 给定一个默认的页面,ongenerateinitialroutes 用于生产启动应用时的路由列表,它有一个默认实现 defaultgenerateinitialroutes,会根据传递的 initialroutename 选择不同的 route,如果传入的 initialroutename 并不是默认的主页面路由 navigator.defaultroutename,flutter 并不会将 initroute 作为主页面,而是将默认路由入栈了之后再入栈 initroute 对应的页面,所以如果在这之后再调用 poproute,是会返回到主页面的
observers 是路由切换的监听列表,可以由外部传入,在路由切换的时候做些操作,比如 herocontroller 就是一个监听者。
navigator 是一个 statefulwidget,在 navigatorstate 的 initstate 中完成了将 initroute 转换成 route 的过程,并调用 push 将其入栈,生成 overlayentry,这个会继续传递给下层负责显示页面的 overlay 负责展示。
在 push 的过程中,route 会被转换成 overlayentry 列表存放,每一个 overlayentry 中存储一个 widgetbuilder,从某种角度来说,overlayentry 可以被认为是一个页面。所有的页面的协调、展示是通过 overlay 完成的,overlay 是一个类似于 stack 的结构,它可以展示多个子结点。在它的 initstate 中,
会将 initialentries 都存到 _entries 中。
overlay 作为一个能够根据路由确定展示页面的控件,它的实现其实比较简单:
build 函数中,将所有的 overlayentry 分成了可见与不可见两部分,每一个 overlayentry 生成一个 _overlayentry,这是一个 statefulwidget,它的作用主要是负责控制当前页重绘,都被封装成 然后再用 _theatre 展示就完了,在 _theatre 中,可见/不可见的子结点都会转成 element,但是在绘制的时候,_theatre 对应的 _rendertheatre 只会把可见的子结点绘制出来。
判断某一个 overlayentry 是否能够完全遮挡上一个 overlayentry 是通过它的 opaque 变量判断的,而 opaque 又是由 route 给出的,在页面动画执行时,这个值会被设置成 false,然后在页面切换动画执行完了之后就会把 route 的 opaque 参数赋值给它的 overlayentry,一般情况下,窗口对应的 route 为 false,页面对应的 route 为 true。
所以说在页面切换之后,上一个页面始终都是存在于 element 树中的,只不过在 renderobject 中没有将其绘制出来,这一点在 flutter outline 工具里面也能够体现。从这个角度也可以理解为,在 flutter 中页面越多,需要处理的步骤就越多,虽然不需要绘制底部的页面,但是整个树的基本遍历还是会有的,这部分也算是开销。
_routenamed
flutter 中进行页面管理主要的依赖路由管理系统,它的入口就是 navigator,它所管理的东西,本质上就是承载着用户页面的 route,但是在 navigator 中有很多函数是 xxxname 系列的,它们传的不是 route,而是 routename,据个人理解,这个主要是方便开发引入的,我们可以在 materialapp/cupertinoapp 中直接传入路由表,每一个名字对应一个 widgetbuilder,然后结合 pageroutebuilder(这个可以自定义,不过 materialapp/cupertinoapp 都有默认实现,能够将 widgetbuilder 转成 route),便可以实现从 routename 到 route 的转换。
这个过程分三步,生成 routesettings,调用 ongenerateroute 从路由表中拿到对应的路由,如果无命中,就调用 onunknownroute 给一个类似于 404 页面的东西。
ongenerateroute 和 onunknownroute 在构建 navigator 时传入,在 widgetsapp 中实现,
如果是默认的路由会直接使用给定的 home 页面(如果有),否则就直接到路由表查,所以本质上这里的 home 页面更多的是一种象征,身份的象征,没有也无所谓。另外路由表主要的产出是 widgetbuilder,它需要经过一次包装,成为 route 才是成品,或者如果不想使用路由表这种,也可以直接实现 ongenerateroute 函数,根据 routesetting 直接生成 route,这个就不仅仅是返回 widgetbuilder 这么简单了,需要自己包装。
onunknownroute 主要用于兜底,提供一个类似于 404 的页面,它也是需要直接返回 route。
_flushhistoryupdates
不知道从哪一个版本开始,flutter 的路由管理引入了状态,与之前每一个 push、pop 都单独实现不同,所有的路由切换操作都是用状态表示,同时所有的 route 都被封装成 _routeentry,它内部有着关于 route 操作的实现,但都被划分为比较小的单元,且都依靠状态来执行。
状态是一个具有递进关系的枚举,每一个 _routeentry 都有一个变量存放当前的状态,在 _flushhistoryupdates 中会遍历所有的 _routeentry 然后根据它们当前的状态进行处理,同时处理完成之后会切换它们的状态,再进行其他处理,这样的好处很明显,所有的路由都放在一起处理之后,整个流程会变得更加清晰,且能够很大程度上进行代码复用,比如 push 和 pushreplacement 两种操作,这在之前是需要在两个方法中单独实现的,而现在他们则可以放在一起单独处理,不同的只有后者比前者会多一个 remove 的操作。
关于 _flushhistoryupdates 的处理步骤:
以上是除了状态处理之外,一次 _flushhistoryupdates 的全过程,首先它会遍历整个路由列表,根据状态做不同的处理,不过一般能够处理到的也不过最上层一两个,其余的多半是直接跳过的。处理完了之后,调用 _flushrouteannouncement 进行路由之间的前后链接,比如进行动画的联动等,
其实现也比较清晰,对每一个 _routeentry,通过调用 didchangenext 和 didchangeprevious 来建立联系,比如在 didchangenext 中绑定当前 route 的 secondaryanimation 和下一个路由的 animation 进行动画联动,再比如在 didchangeprevious 中获取上一个路由的 title,这个可以用于 cupertinonavigationbar 中 back 按钮展示上一页面的 title。
然后调用 maybenotifyroutechange 发出通知,指定当前正在处于展示状态的 route。
最后,遍历 tobedisposed 执行 _routeentry 的销毁,这个列表会保存上面循环处理过程中,确定需要移出的 _routeentry,通过调用 overlayentry remove 函数(它会将自己从 overlay 中移除)和 overlayentry dispose 函数(它会调用 route 的 dispose,进行资源释放,比如 transitionroute 中 animationcontroller 销毁)。
最后再看关于状态的处理,以下是所有的状态:
本质上这些状态分为三类,add(处理初始化的时候直接添加),push(与 add 类似,但是增加了动画的处理),pop(处理页面移出),remove(移出某个页面,相对 pop 没有动画,也没有位置限制)。
add
add 方式添加路由目前还只用于在应用初始化是添加初始化页面使用,对应的是在 navigatorstate 的 initstate 中,
它会将从 ongenerateinitialroutes 得来的所有初始路由转成 _routeentry 加入到 _history,此时它们的状态是 _routelifecycle.add,然后就是调用 _flushhistoryupdates 进行处理。
add 路线主要会调用两个函数,handleadd 和 didadd,
install 函数可以看作是 route 的初始化函数,比如在 modalroute 中创建 proxyanimation 来管理一些动画的执行,在 transitionroute 中创建了用于执行切换动画的 animationcontroller,在 overlayroute 中完成了当前 route 的 overlayentry 的创建及插入。createoverlayentries 用于创建 overlayentry,其实现在 modalroute,
每一个 route 都能生成两个 overlayentry,一个是 _buildmodalbarrier,它可以生成两个页面之间的屏障,我们可以利用它给新页面设置一个背景色,同时还支持动画过渡,另一个是 _buildmodalscope,它生成的就是这个页面真正的内容,外部会有多层包装,最底层就是 widgetbuilder 创建的 widget。
大致看下两个函数的实现,
modalbarrier 是两个 route 之间的屏障,它可以通过颜色、拦截事件来表示两个 route 的隔离,这些都是可以配置的,这里 ignorepointer 的作用是为了在执行切换动画的时候无法响应时间。
_modalscope 需要承载用户界面的展示,它的 build 函数可以看到在 widget.route.buildpage 出用户定义的页面之上有很多层,可以一层一层看下大致作用:
- _modalscopestatus,继承自 inheritedwidget,用于给底层结点提供数据
- offstage,可以通过 offstage 变量控制是否绘制
- pagestorage,它提供了一种存储策略,也就是 pagestoragebucket,这个类可以给某一个 buildcontext 绑定特定的数据,支持写入和读取,可用于某一个 widget 的状态存储等
- focusscope,用于焦点管理用,一般只有获取焦点的控件才能接收到按键信息等
- repaintboundary,控制重绘范围,意在减少不必要的重绘
- animatedbuilder,动画控制 widget,会根据 animation 进行 rebuild
- widget.route.buildtransitions,它在不同的 route 中可以有不同的实现,比如 android 的默认实现是自下向上渐入,ios 的默认实现是自右向左滑动,另外也可以通过自定义 route 或自定义 themedata 实现自定义的切换动画,还有一点需要说明,route 中的动画分为 animation 和 secondaryanimation,其中 animation 定义了自己 push 时的动画,secondaryanimation 定义的是新页面 push 时自己的动画,举个例子,在 ios 风格中,新页面自右向左滑动,上一个页面也会滑动,此时控制上一个页面滑动的动画就是 secondaryanimation
- ignorepointer,同样是用于页面切换动画执行中,禁止用户操作
- repaintboundary,这里的考量应该是考虑到上层有一个动画执行,所以这里包一下避免固定内容重绘
- builder,builder 的唯一作用应该是提供 buildcontext,虽然说每一个 build 函数都有 buildcontext 参数,但这个是当前 widget 的,而不是直属上级的,这可能有点抽象,比如说下面的 buildpage 需要使用 buildcontext 作为参数,那么如果它需要使用 context 的 ancestorstateoftype 的话,实际上就是从 _modalscopestate 开始向上查找,而不是从 builder 开始向上查找
- widget.route.buildpage,这个函数内部就是使用 route 的 widgetbuilder 创建用户界面,当然不同的 route 可能还会在这里再次进行包装
以上就是一个页面中,从 overlay(说是 overlay 不是那么合理,但是在此先省略中间的 _theatre 等) 往下的布局嵌套。新的 overlayentry 创建完成之后,会把它们都传递到 overlay 中,且在这个过程中会调用 overlay 的 setstate 函数,请求重新绘制,在 overlay 中实现新旧页面的切换。
以上是 install 的整个过程,执行完了之后把 currentstate 置为 adding 返回。
此处有一点需要注意,while 循环会自上往下遍历所有的 _routeentry,但是当一个连续操作尚未完成时,它是不会去执行下一个 _routeentry 的,其实现就在于代码中的 continue 关键字,这个关键字会直接返回执行下一次循环,但是并没有更新当前 _routeentry,所以实际处理的还是同一个路由,这种一般用于 _routeentry 状态发生变化,且需要连续处理的时候,所以对于 add 来说,执行完了之后会立刻执行 adding 代码块,也就是 didadd,
route 的 didadd 函数表示这个路由已经添加完成,它会做一些收尾处理,比如在 transitionroute 中更新 animationcontroller 的值到最大,并设置透明等。然后 didadd 将状态置为 idle,并调用所有监听者的 didpush。idle 表示一个 _routeentry 已经处理完毕,后续只有 pop、replace 等操作才会需要重新处理,add 过程到这里也可以结束了。
push
push 过程就是将 route 封装成 _routeentry 加入到 _history 中并调用 _flushhistoryupdates,它的初始状态时 push,并在最后返回 route.popped,这是一个 future 对象,可以用于前一个页面接收新的页面的返回结果,这个值是在当前路由 pop 的时候传递的。
这里将 push、pushreplace、replace 都归为了一类,它会先调用 handlepush,这个函数中其实包含了 add 过程中的 handleadd、didadd 两个函数的功能,比如调用 install、调用 didpush,不同的是,push/pushreplace 会有一个过渡的过程,即先执行切换动画,此时它的状态会变为 pushing,并在动画执行完时切到 idle 状态并调用 _flushhistoryupdates 更新,而 replace 则直接调用 didreplace 完成页面替换,从这里看,这个应该是没有动画过渡的。后面还是一样,调用通知函数。
pop
pop 的过程与上面两个不太一样,它在 navigatorstate.pop 中也有一些操作:
就是调用 _routeentry 的 pop,在这个函数中它会调用 route 的 didpop,完成返回值的传递、移出动画启动等。但是在 overlayroute 中:
finalizeroute 的调用需要依赖 finishedwhenpopped 的值,这个值在子类中可以被修改,比如 transitionroute 中它就是 false,理解也很简单,在 transitionroute 中执行 didpop 之后也不能直接就销毁 route,而是先要执行移出动画,而如果不需要执行动画,则可以直接调用,否则就在动画执行完再执行,这一点是通过监听动画状态实现的,在 transitionroute 中。
在 finalizeroute 中,它会判断是否正在 pop 过程中,如果是,就说明此刻是直接调用的 finalizeroute,那就需要先执行 pop 状态的操作,再执行 dispose 操作,将状态切换到 dispose 进行处理,如果不是,就说明调用这个函数的时候,是动画执行完的时候,那么此刻 pop 状态处理已经完成,所以跳过了 pop 处理的步骤,如上。下面就看一下 pop 过程做的处理。
handlepop 将状态切换到 poping(动画执行过程),然后发出通知,而 poping 状态不作处理,因为这是一个过渡状态,在动画执行完之后会自动切换到 dispose 状态,同样的,上面的 pushing 状态也是,而在 dispose 分支中,就是将 _routeentry 从 _history 移除并加入到 tobedisposed,然后在遍历结束之后统一销毁。
remove
remove 的逻辑就是先从 _history 中找到一个跟传进来的一致的 _routeentry,将它的状态设为 remvoe,再调用 _flushhistoryupdates。
首先会调用 handleremoval,调用通知,并将状态切换到 removing,在 removing 阶段再将状态切到 dispose,然后就是将其加入 tobedisposed,所以整个过程中是不涉及动画的,一般只用来移出非正在展示的页面,否则还是推荐用 pop。
总结
以上是路由机制的实现原理,就其整体而言,最给人耳目一新的就是状态管理的加入,通过将一个页面的进出划分到不同状态处理,是能够有效降低代码的复杂度的,不过从目前的结果来看,这一个过程执行的还不够精炼,比如状态的划分不够合理,从这些状态的设计来看,add/push/pop 都有对应的 ing 形式表示正在执行中,但是 adding 的存在我暂时没有看到必要性,还有就是感觉代码的组织上还是有点问题,比如 handleadd 与 handpush 实际上还有很大部分的代码重复的,这部分不知道以后会不会优化。
另外还有一点感觉做的不到位,就是 _routenamed 这个函数没有对外开放,而且并不是所有的路由操作都提供了 name 为入参的包装,比如 removeroute,在这种情况下就没法很方便的调用。
到此这篇关于flutter 路由机制的实现的文章就介绍到这了,更多相关flutter 路由机制内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
下一篇: php二维数组怎么增加键值