BotToast ?
一個真正意義上的flutter Toast庫!
?特點
-
真正意義上的Toast,可以在任何你需要的時候呼叫,不會有任何限制! (這個特性是筆者寫一個bot_toast主要一大誘因,因為github上很多flutter Toast 在某些方法是不能呼叫的比如說initState生命週期方法)
-
功能豐富,支援顯示通知,文字,載入,附屬等型別Toast
-
支援在彈出各種自定義Toast,或者說你可以彈出任何Widget,只要它符合flutter程式碼的要求即可
-
Api簡單易用,基本上沒有必要引數(包括BuildContext),基本上都是可選引數
-
純flutter實現
?例子
線上例子(Online demo) (Web效果可能有偏差,真實效果請以手機端為準)
?效果圖
Notification | Attached |
---|---|
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();
}
複製程式碼
沒錯和你想的一樣,沒有報錯正常顯示了。
為什麼會這樣子捏,看看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
的呢?
因為_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部分終於落下帷幕。✌
Widget/Element部分
其實這部分的的流程和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部分也到此結束啦(噢耶,終於快寫完了?)
4. 煉製bot_toast
咻咻,煉製成功,恭喜你得到了bot_toast和一大堆原始碼?
結語
- 開源不易,寫文章也不易,這篇文章斷斷續續寫一個星期,希望大家都能有不同的收穫。
- 如果覺得這篇文章或者bot_toast不錯的話,動動小手給個?,就是對我最大的鼓勵。?
- 如果文章有不當之處,寫的不好的地方歡迎指出。
- 如果要閱讀Flutter原始碼推薦從XxxxBinding開始看,自頂而下看減低閱讀難度