Flutter bot_toast是怎样炼成的

3,843 阅读8分钟

BotToast 💥

一个真正意义上的flutter Toast库!

🐶特点

  • 真正意义上的Toast,可以在任何你需要的时候调用,不会有任何限制! (这个特性是笔者写一个bot_toast主要一大诱因,因为github上很多flutter Toast 在某些方法是不能调用的比如说initState生命周期方法)

  • 功能丰富,支持显示通知,文本,加载,附属等类型Toast

  • 支持在弹出各种自定义Toast,或者说你可以弹出任何Widget,只要它符合flutter代码的要求即可

  • Api简单易用,基本上没有必要参数(包括BuildContext),基本上都是可选参数

  • 纯flutter实现

🐼例子

在线例子(Online demo) (Web效果可能有偏差,真实效果请以手机端为准)

🐺效果图

Notification Attached
Notification
Attached
Loading Text
Loading
Text

🐳快速使用及文档

点击这里查看,不做展开



🐸 炼成原理

没错,披着bot_toast外皮讲源码的正是在下🤠

1. 炼成原材料

  • Overlay

  • SchedulerBinding

2. Overlay

2.1 Overlay是什么?

从字面意思看就是覆盖,而Overlay也确实具有如此能力。我们可以通过Overlay.of(context).insert(OverlayEntry(builder: (_)=>Text("i miss you")))方法插入一个Widget覆盖原来的页面上,其效果等同于Stack,其内部其实也使用了Stack,更详细的解释可以看这篇文章,这里不多做展开。

2.2 那Overlay跟我们通过Navigator.[push,pop]的页面有什么关系?


修正:其实下面内容有误,当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;
  }

3. SchedulerBinding

3.1 什么是SchedulerBinding?

很明显看名字就知道是跟调度有关的。主要有几个api:

  • SchedulerBinding.instance.scheduleFrameCallback 添加一个瞬态帧回调,主要给动画使用
  • SchedulerBinding.instance.addPersistentFrameCallback 添加一个持久帧回调,添加后不可以取消,像build/layout/paint等方法都是在这里得到执行(为什么我会知道呢,下面会深入分析为什么是这里执行)
  • SchedulerBinding.instance.addPostFrameCallback 添加一个在帧结束前的回调

它们的执行顺序是: scheduleFrameCallback->addPersistentFrameCallback->addPostFrameCallback

3.2 SchedulerBinding有什么用?

在解释有什么用之前,先看一段代码

 @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();
  }

没错和你想的一样,没有报错正常显示了。

iloveyou
为什么会这样子捏,看看3.1的执行顺序就知道通过addPostFrameCallback()添加的方法会在整颗树build完后才去执行。

3.2.1那为什么执行顺序是这样呢?

其实这里有两部分:layout/paint和build,也就是RenderObject和Widget/Element两部分,先讲前者

RenderObject部分
  • 在有了SchedulerBinding的基础上,我们把视线转到RendererBinding

看看它的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的呢?

因为_nodesNeedingLayoutPipelineOwner所持有的,而RendererBinding持有一个PipelineOwner,所以还是看回RendererBindinginitInstances方法,发现一个重要的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部分终于落下帷幕。✌


Widget/Element部分

其实这部分的的流程和RenderObject部分有些相似,也是有一个BuildOwner(对应着上面PipelineOwner),也是有一个attachToRenderTree方法(对应着上面attach)

首先还是解释为什么build是在SchedulerBinding.instance.addPersistentFrameCallback里调用的,直接看WidgetsBinding,在这里主要关注两件事:

  1. 创建BuildOwner
  2. 重写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发现其中在就是调用了每个脏Elementrebuild方法,而rebuild又会调用performRebuild方法,这个方法会被子类重写,主要看ComponentElement.performRebuild就行,因为StatefulElementStatelessElement都是继承此类.而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,并且在每个子Elementmount时将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部分也到此结束啦(噢耶,终于快写完了😂)


4. 炼制bot_toast

咻咻,炼制成功,恭喜你得到了bot_toast和一大堆源码😉


结语

  1. 开源不易,写文章也不易,这篇文章断断续续写一个星期,希望大家都能有不同的收获。
  2. 如果觉得这篇文章或者bot_toast不错的话,动动小手给个👍,就是对我最大的鼓励。😊
  3. 如果文章有不当之处,写的不好的地方欢迎指出。
  4. 如果要阅读Flutter源码推荐从XxxxBinding开始看,自顶而下看减低阅读难度