真正意義上的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 |
---|---|
![]() |
![]() |
點擊這裏查看,不作展開app
沒錯,披着bot_toast外皮講源碼的正是在下🤠less
Overlayide
SchedulerBindingpost
從字面意思看就是覆蓋,而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和一大堆源碼😉