Flutter bot_toast是怎樣煉成的

mmmzq發表於2019-07-20

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開始看,自頂而下看減低閱讀難度

相關文章