真正意义上的Toast,能够在任何你须要的时候调用,不会有任何限制! (这个特性是笔者写一个bot_toast主要一大诱因,由于github上不少flutter Toast 在某些方法是不能调用的好比说initState生命周期方法)node
功能丰富,支持显示通知,文本,加载,附属等类型Toastgit
支持在弹出各类自定义Toast,或者说你能够弹出任何Widget,只要它符合flutter代码的要求便可程序员
Api简单易用,基本上没有必要参数(包括BuildContext),基本上都是可选参数github
纯flutter实现api
在线例子(Online demo) (Web效果可能有误差,真实效果请以手机端为准)bash
Notification | Attached |
---|---|
![]() |
![]() |
Loading | Text |
---|---|
![]() |
![]() |
点击这里查看,不作展开markdown
没错,披着bot_toast外皮讲源码的正是在下🤠app
Overlayless
SchedulerBindingide
从字面意思看就是覆盖,而Overlay
也确实具备如此能力。咱们能够经过Overlay.of(context).insert(OverlayEntry(builder: (_)=>Text("i miss you")))
方法插入一个Widget
覆盖原来的页面上,其效果等同于Stack
,其内部其实也使用了Stack
,更详细的解释能够看这篇文章,这里很少作展开。
修正:其实下面内容有误,当Navigator
的Route集合为空时,再push Route时这个路由会“错误”的插入到Overlay所持有OverlayEntry
的最后面
2019/7/22修正
其实Navigator
内部也使用了Overlay
。通常经过Overlay.of(context)
获取到的Overlay
都是Navigator
所建立的Overlay
。
使用Navigator
所建立的Overlay
会有一个特色就是咱们手动使用Overlay.of(context).insert
方法插入一个Widget
的话,该Widget
会一直覆盖在Navigator
全部Route页面上.
究其缘由就是Navigator
动了手脚(没想到它是这样的Navigator
😲),当咱们Push一个Route的时候,Route会转化为两个OverlayEntry
,一个不是特别重要的遮罩OverlayEntry
,一个就是包含咱们新页面的OverlayEntry
。而Navigator
有一个List<Route>
来保存全部路由,一个路由持有两个OverlayEntry
。新push进来的两个OverlayEntry
会插入到Navigator
所持有OverlayEntry
集合的最后一个OverlayEntry
后面 (注意不是Overlay所持有OverlayEntry
的最后面) ,这样就能保证咱们手动经过Overlay.of(context).insert
方法插入的Widget老是在全部Route页面上面,是否是如今看的云里雾里,图来了🤩。
@optionalTypeArgs Future<T> push<T extends Object>(Route<T> route) { ... final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null; route._navigator = this; route.install(_currentOverlayEntry); //<----获取当前OverlayEntry,一般状况也就是最后一个OverlayEntry ... } 复制代码
OverlayEntry get _currentOverlayEntry { for (Route<dynamic> route in _history.reversed) { if (route.overlayEntries.isNotEmpty) return route.overlayEntries.last; } return null; } 复制代码
很明显看名字就知道是跟调度有关的。主要有几个api:
SchedulerBinding.instance.scheduleFrameCallback
添加一个瞬态帧回调,主要给动画使用SchedulerBinding.instance.addPersistentFrameCallback
添加一个持久帧回调,添加后不能够取消,像build/layout/paint等方法都是在这里获得执行(为何我会知道呢,下面会深刻分析为何是这里执行)SchedulerBinding.instance.addPostFrameCallback
添加一个在帧结束前的回调它们的执行顺序是: scheduleFrameCallback->addPersistentFrameCallback->addPostFrameCallback
在解释有什么用以前,先看一段代码
@override void initState() { Overlay.of(context).insert(OverlayEntry(builder: (_)=>Text("i love you"))); super.initState(); } 复制代码
你会发现上面这段代码会直接报错 报错内容以下,大概意思在孩子构建过程当中调用了父类的setState()或者 markNeedsBuild()方法(注意这段解释可能不许确,仅供参考)
The following assertion was thrown building Builder: setState() or markNeedsBuild() called during build. This Overlay widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase. 复制代码
再看看使用了SchedulerBinding的话会发生什么?
@override void initState() { SchedulerBinding.instance.addPostFrameCallback((_){ Overlay.of(context).insert(OverlayEntry(builder: (_)=>Text("i love you"))); }); super.initState(); } 复制代码
没错和你想的同样,没有报错正常显示了。
addPostFrameCallback()
添加的方法会在整颗树build完后才去执行。
其实这里有两部分:layout/paint和build,也就是RenderObject和Widget/Element两部分,先讲前者
看看它的initInstances
@override void initInstances() { ... addPersistentFrameCallback(_handlePersistentFrameCallback); //调用addPersistentFrameCallback _mouseTracker = _createMouseTracker(); } 复制代码
再看看_handlePersistentFrameCallback,发现最终会调用drawFramed方法
@protected void drawFrame() { assert(renderView != null); pipelineOwner.flushLayout(); pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint(); renderView.compositeFrame(); // this sends the bits to the GPU pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. } 复制代码
看名字就知道和layout和paint有关,看看flushLayout方法就会发现最终会调用了RenderObject.performLayout方法
void flushLayout() { .... try { // TODO(ianh): assert that we're not allowing previously dirty nodes to redirty themselves while (_nodesNeedingLayout.isNotEmpty) { final List<RenderObject> dirtyNodes = _nodesNeedingLayout; //保持着须要从新layout/paint的RenderObject _nodesNeedingLayout = <RenderObject>[]; for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) { if (node._needsLayout && node.owner == this) node._layoutWithoutResize(); } } ... } 复制代码
void _layoutWithoutResize() { ... try { performLayout(); markNeedsSemanticsUpdate(); } catch (e, stack) { _debugReportException('performLayout', e, stack); } ... markNeedsPaint(); } 复制代码
其实咱们这一步已经确认了layout是在SchedulerBinding.instance.addPersistentFrameCallback
调用的,paint也是相似的就再也不分析了。虽然到这里已经足够,可是对于咱们这些热爱学习的程序员怎么够能呢😭。又提出一个疑问:须要从新layout/paint的RenderObject
是怎么添加到_nodesNeedingLayout
的呢?
由于_nodesNeedingLayout
是PipelineOwner
所持有的,而RendererBinding
持有一个PipelineOwner
,因此仍是看回RendererBinding
的initInstances
方法,发现一个重要的initRenderView
@override void initInstances() { ... initRenderView(); ... } 复制代码
从initRenderView
方法一直顺藤摸瓜发现最终生成一个RenderView
并赋给PipelineOwner.rootNode
,而rootNode是一个set方法最终会调用RenderObject.attach
,让RenderObject
持有PipelineOwner
的引用,经过这个引用就能够往_nodesNeedingLayoutt添加脏RenderObject
。
//-------------------------RendererBinding //1. void initRenderView() { assert(renderView == null); renderView = RenderView(configuration: createViewConfiguration(), window: window);//重点 renderView.scheduleInitialFrame(); } PipelineOwner get pipelineOwner => _pipelineOwner; PipelineOwner _pipelineOwner; RenderView get renderView => _pipelineOwner.rootNode; //2. set renderView(RenderView value) { assert(value != null); _pipelineOwner.rootNode = value; } //-------------------------PipelineOwner //3. set rootNode(AbstractNode value) { if (_rootNode == value) return; _rootNode?.detach(); _rootNode = value; _rootNode?.attach(this); } //----------------------RenderObject //4. void attach(covariant Object owner) { assert(owner != null); assert(_owner == null); _owner = owner; } 复制代码
举个🌰:RenderObject.markNeedsLayout
的实现
void markNeedsLayout() { ... if (_relayoutBoundary != this) { markParentNeedsLayout(); } else { _needsLayout = true; if (owner != null) { ... owner._nodesNeedingLayout.add(this); //往脏列表添加自身 owner.requestVisualUpdate(); //会申请调用渲染新一帧保证drawFrame获得调用 } } } 复制代码
到这里RenderObject部分终于落下帷幕。✌
其实这部分的的流程和RenderObject部分有些类似,也是有一个BuildOwner
(对应着上面PipelineOwner
),也是有一个attachToRenderTree
方法(对应着上面attach
)
首先仍是解释为何build
是在SchedulerBinding.instance.addPersistentFrameCallback
里调用的,直接看WidgetsBinding
,在这里主要关注两件事:
BuildOwner
drawFrame
方法BuildOwner get buildOwner => _buildOwner; final BuildOwner _buildOwner = BuildOwner(); @override void drawFrame() { ... try { if (renderViewElement != null) buildOwner.buildScope(renderViewElement); //重点是这里 super.drawFrame(); buildOwner.finalizeTree(); } ... ... } 复制代码
查看BuildOwner.buildScope
发现其中在就是调用了每一个脏Element
的rebuild
方法,而rebuild
又会调用performRebuild
方法,这个方法会被子类重写,主要看ComponentElement.performRebuild
就行,由于StatefulElement
和StatelessElement
都是继承此类.而ComponentElement.performRebuild
最终又会调用Widget.build
/State.build
也就是咱们常写的build方法
//----------------------------BuildOwner void buildScope(Element context, [ VoidCallback callback ]) { ... _dirtyElements.sort(Element._sort); _dirtyElementsNeedsResorting = false; int dirtyCount = _dirtyElements.length; int index = 0; while (index < dirtyCount) { ... try { _dirtyElements[index].rebuild(); //重点 } catch (e, stack) { ... } ... } ... } //----------------------------Element void rebuild() { ... performRebuild(); .. } //---------------------------ComponentElement @override void performRebuild() { ... try { built = build(); debugWidgetBuilderValue(widget, built); } ... ... } 复制代码
至此到这里能够确认build
是在SchedulerBinding.instance.addPersistentFrameCallback
里调用的,可是身为高贵的程序单身狗怎么会知足呢,咱们须要知道更多!🐶
脏Element
是怎么添加到BuildOwner._dirtyElements
里面的?
没错和RenderObject部分也是有些类似,只不过启动入口变了,变到了runApp
方法去了
直接看runApp
代码发现attachRootWidget
很显眼很特殊,一步步查看发现最终调用了RenderObjectToWidgetAdapter.attachToRenderTree
方法上去了,也正是这个方法将WidgetsBinding.BuildOwner
传递给了根Element
也就是RenderObjectToWidgetElement
,而且在每一个子Element
mount时将WidgetsBinding.BuildOwner
也分配给子Element
,这样整颗Element树的每个Element都持有了BuildOwner
,每一个Element
都拥有将自身标记为脏Element
的能力
//---------------runApp //1. void runApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() ..attachRootWidget(app) //重点 ..scheduleWarmUpFrame(); } //2. void attachRootWidget(Widget rootWidget) { _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>( container: renderView, debugShortDescription: '[root]', child: rootWidget, ).attachToRenderTree(buildOwner, renderViewElement); //重点 } //-------------------RenderObjectToWidgetAdapter //3. RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) { if (element == null) { owner.lockState(() { element = createElement(); //建立根Element assert(element != null); element.assignOwner(owner); //根Element拿到BuildOwner引用 }); owner.buildScope(element, () { element.mount(null, null); }); }... return element; } //---------------------Element //4. void mount(Element parent, dynamic newSlot) { ... _parent = parent; _slot = newSlot; _depth = _parent != null ? _parent.depth + 1 : 1; _active = true; if (parent != null) // Only assign ownership if the parent is non-null _owner = parent.owner; //子Element拿到父Element的BuildOwner引用 ... } 复制代码
Widget/Element部分也到此结束啦(噢耶,终于快写完了😂)
咻咻,炼制成功,恭喜你获得了bot_toast和一大堆源码😉