Flutter bot_toast是怎樣煉成的

BotToast 💥

一個真正意義上的flutter Toast庫!

🐶特色

  • 真正意義上的Toast,能夠在任何你須要的時候調用,不會有任何限制! (這個特性是筆者寫一個bot_toast主要一大誘因,由於github上不少flutter Toast 在某些方法是不能調用的好比說initState生命週期方法)node

  • 功能豐富,支持顯示通知,文本,加載,附屬等類型Toastgit

  • 支持在彈出各類自定義Toast,或者說你能夠彈出任何Widget,只要它符合flutter代碼的要求便可程序員

  • Api簡單易用,基本上沒有必要參數(包括BuildContext),基本上都是可選參數github

  • 純flutter實現api

🐼例子

在線例子(Online demo) (Web效果可能有誤差,真實效果請以手機端爲準)bash

🐺效果圖

Notification Attached
Notification
Attached
Loading Text
Loading
Text

🐳快速使用及文檔

點擊這裏查看,不作展開markdown



🐸 煉成原理

沒錯,披着bot_toast外皮講源碼的正是在下🤠app

1. 煉成原材料

  • Overlayless

  • SchedulerBindingide

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開始看,自頂而下看減低閱讀難度
相關文章
相關標籤/搜索