老孟導讀:這是2021年源碼系列的第一篇文章,其實源碼系列的文章不是特別受歡迎,一個緣由是原理性的知識很是枯燥,我本身看源碼的時候特別有感觸,二是想把源碼分析講的通俗易懂很是困難,本身明白 和 讓別人聽懂徹底是兩回事。不過我依然會堅持 Flutter 源碼系列的文章,提升本身的技術水平的同時,也但願你們收穫一些知識。
爲了使源碼系列的文章不那麼枯燥,文章中會有不少流程圖,流程圖比純文字更直觀,一圖勝千言。html
我也是第一次寫源碼系列的文章,若是文章哪裏有不對的地方請告訴我,雖然我也不必定聽😄,開個玩笑。git
但願你們來個 贊,您的 贊是我寫下去的巨大動力😄。github
全部源碼系列文章都會分享到我我的博客:http://laomengit.com/面試
注意:使用的 Flutter 版本 和 Dart 版本以下:Flutter 1.22.4 • channel stable • https://github.com/flutter/fl...
Framework • revision 1aafb3a8b9 (6 weeks ago) • 2020-11-13 09:59:28 -0800
Engine • revision 2c956a31c0
Tools • Dart 2.10.4服務器不一樣的版本可能有所不一樣,請注意版本之間的區別。微信
首先, InheritedWidget 是一個很是重要,很是重要,很是重要的組件,重要的事情說3遍😄,系統中不少功能都是功能型組件都是經過 InheritedWidget 實現的,著名的 Provider 狀態管理框架也是基於 InheritedWidget 實現的,所以不論是工做中,仍是面試,InheritedWidget 組件的原理及使用場景都是考察的重點。app
此篇文章包括以下幾個部分:框架
InheritedWidget 組件是功能型組件,提供了沿樹向下,共享數據的功能,即子組件能夠獲取父組件(InheritedWidget 的子類)的數據,經過BuildContext.dependOnInheritedWidgetOfExactType 獲取。less
InheritedWidget 組件的共享數據是沿着樹從上到下,是否聯想到 Notification,Notification 正好與 InheritedWidget 傳遞方向相反,Notification 是沿着樹從下到上,二者功能的實現都是子節點主動發起,InheritedWidget 組件的子節點主動查找父節點上 InheritedWidget 共享的數據,Notification 也是子節點主動發起通知,沿着樹向上通知。ide
Notification 也是 Flutter 中很是重要的,後面會有專門的文章詳細介紹,此篇不作介紹。
那麼什麼樣的場景適合使用 InheritedWidget 呢? 一般App會有登陸用戶信息,登陸用戶信息爲全局共享信息,想象一下,子組件要如何獲取登陸用戶信息?將上面的場景抽象一下,有一顆組件樹,A、H 組件依賴同一數據,以下:
A、H 組件要如何獲取到數據呢?
有一種實現方式是 經過構造函數透傳,數據經過A傳遞給B,B傳遞給C、E,C和E在傳遞給F、H,以下圖虛線的傳遞:
反應到代碼上就是:
return A( data:data child:B( data:data child:C( data:data child:F( data:data ) ) ) );
這樣的實現缺點很是明顯,B、C組件不須要 data 數據,若是組件樹比較深的話,那將是噩夢。
爲了處理此問題,Flutter Framework 提供了 InheritedWidget 組件,InheritedWidget 組件的子組件能夠直接獲取數據,以下圖:
InheritedWidget 組件的全部子組件均可以直接經過 BuildContext.dependOnInheritedWidgetOfExactType 獲取數據。
上面分析了 InheritedWidget 組件的使用場景,下面用一個最簡單的 demo 展現如何使用 InheritedWidget 組件。
定一個用戶信息共享數據的實體類,任何子組件均可以獲取用戶信息,用戶信息實體類:
class UserInfo { String name; int age; UserInfo({this.name, this.age}); @override bool operator ==(Object other) { if (!(other is UserInfo)) { return false; } var old = other as UserInfo; return name == old.name && age == old.age; } }
UserInfo 類重寫了 == 操做符,是爲了後面數據是否發生變化作判斷。
定義共享 UserInfo 數據的 InheritedWidget 組件。
class MyInheritedWidget extends InheritedWidget { final UserInfo userInfo; MyInheritedWidget({this.userInfo, Widget child}):super(child: child); static MyInheritedWidget of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>(); } @override bool updateShouldNotify(covariant MyInheritedWidget oldWidget) { return oldWidget.userInfo != userInfo; } }
這裏有兩個地方須要注意:
靜態(static) of 方法,這個方法不是必須的,但通常都會添加此方法,方便其子組件調用,固然也能夠直接在子組件中使用 context.dependOnInheritedWidgetOfExactType 方法,不加 of 方法,子組件調用以下:
class F extends StatelessWidget { @override Widget build(BuildContext context) { var myInheritedWidget = context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>(); return Text('name:${myInheritedWidget.userInfo.name}'); } }
添加靜態(static) of 方法,用法以下:
class F extends StatelessWidget { @override Widget build(BuildContext context) { return Text('name:${MyInheritedWidget.of(context).userInfo.name}'); } }
咱們常用的 MediaQuery.of(context) 和 Theme.of(context) 方法都是系統封裝的此方法。
使用 MyInheritedWidget 組件:
class InheritedWidgetDemo extends StatefulWidget { @override _InheritedWidgetDemoState createState() => _InheritedWidgetDemoState(); } class _InheritedWidgetDemoState extends State<InheritedWidgetDemo> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('InheritedWidget Demo'), ), body: Center( child: MyInheritedWidget( userInfo: UserInfo(name: '老孟', age: 18), child: A( child: F(), ), ), ), ); } }
A 組件代碼:
class A extends StatelessWidget { final Widget child; const A({Key key, this.child}) : super(key: key); @override Widget build(BuildContext context) { return Center( child: child, ); } }
F 組件代碼:
class F extends StatelessWidget { @override Widget build(BuildContext context) { return Text('name:${MyInheritedWidget.of(context).userInfo.name}'); } }
上面代碼構成的組件樹爲(無關的節點已忽略好比Scaffold、Center等):
注意: A 組件是爲了表示樹的深度,此 Demo 中將其簡化了,僅僅設置了一層,也能夠設多多層。
運行效果:
下面修改數據並刷新UI,下面的代碼僅能用於Demo,是爲了演示方便,千萬不要用於實際項目,由於下面的寫法有巨大的性能問題,由於重建了 InheritedWidget 組件下的全部子組件,重要的事情說3遍:下面演示Demo千萬不要用於實際項目,千萬不要用於實際項目,千萬不要用於實際項目。文章最後我會給出正確用法。
修改 _InheritedWidgetDemoState :
class _InheritedWidgetDemoState extends State<InheritedWidgetDemo> { UserInfo _userInfo = UserInfo(name: '老孟', age: 18); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('InheritedWidget Demo'), ), body: Center( child: MyInheritedWidget( userInfo: _userInfo, child: A( child: F(), ), ), ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { _userInfo = UserInfo(name: '老孟1', age: 18); }); }, ), ); } }
點擊按鈕的時候,UI刷新了,但請重點看右側 rebuild stats 部分,每點擊一次按鈕,MyInheritedWidget 組件及其子組件所有 從新構建(rebuild),但 A 組件並不依賴於 MyInheritedWidget 共享數據,理想狀況下不該該 rebuild,實際項目中,樹的結構會比這個複雜的多,所以所有 rebuild 會形成性能問題,這也是開頭說千萬不要將此方式用於實際項目,網上充斥着大量此種用法的文章,後面會給出正確用法,正確用法比較複雜,並且涉及其餘相關知識,因此此處的Demo僅用於學習 InheritedWidget。
你們是否還記得 Stateful 組件的生命週期 文章中介紹的 didChangeDependencies 生命週期,對其的介紹以下:
didChangeDependencies 方法在 initState 以後由 Framework 當即調用。另外,當此 State 對象的依賴項更改時被調用,好比其所依賴的 InheritedWidget 發生變化時, Framework 會調用此方法通知組件發生變化。
下面將 A 和 F 組件改成 StatefulWidget 組件:
class F extends StatefulWidget { @override _FState createState() => _FState(); } class _FState extends State<F> { @override void initState() { super.initState(); print('F initState'); } @override Widget build(BuildContext context) { print('F build'); return Text('name:${MyInheritedWidget.of(context).userInfo.name}'); } @override void didChangeDependencies() { super.didChangeDependencies(); print('F didChangeDependencies'); } @override void dispose() { super.dispose(); print('F dispose'); } } class A extends StatefulWidget { final Widget child; const A({Key key, this.child}) : super(key: key); @override _AState createState() => _AState(); } class _AState extends State<A> { @override void initState() { super.initState(); print('A initState'); } @override Widget build(BuildContext context) { print('A build'); return Center( child: widget.child, ); } @override void didChangeDependencies() { super.didChangeDependencies(); print('A didChangeDependencies'); } @override void dispose() { super.dispose(); print('A dispose'); } }
給各個生命週期添加日誌打印,從新運行,點擊按鈕,輸出日誌以下:
flutter: A build flutter: F didChangeDependencies flutter: F build
所以,依賴 MyInheritedWidget 組件的 F 組件調用 didChangeDependencies 方法,而 A 組件沒有調用 didChangeDependencies 方法,由於 A 沒有依賴 MyInheritedWidget 組件。
下面再說一個很是容易忽略的地方 MyInheritedWidget.updateShouldNotify方法,通常這樣寫:
@override bool updateShouldNotify(covariant MyInheritedWidget oldWidget) { return oldWidget.userInfo != userInfo; }
這樣寫有什麼問題嗎?若是數據(userInfo)是自定義的實體類且未在 UserInfo 中重寫 ==,那麼極大機率出現有問題,由於不重寫 == 操做符方法,使用 != 判斷是否相等的時候判斷的是兩個對象的內存地址,下面將 UserInfo 中 == 方法去掉,
class UserInfo { String name; int age; UserInfo({this.name, this.age}); }
修改 _InheritedWidgetDemoState 類:
class _InheritedWidgetDemoState extends State<InheritedWidgetDemo> { UserInfo _userInfo = UserInfo(name: '老孟', age: 18); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('InheritedWidget Demo'), ), body: Center( child: MyInheritedWidget( userInfo: _userInfo, child: A( child: F(), ), ), ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { _userInfo = UserInfo(name: '老孟', age: 18); }); }, ), ); } }
修改 updateShouldNotify 方法,添加日誌打印:
@override bool updateShouldNotify(covariant MyInheritedWidget oldWidget) { bool flag = oldWidget.userInfo != userInfo; print('updateShouldNotify:$flag'); return flag; }
點擊按鈕,_userInfo 對象引用發生了變化,但其值( name 和 age)都沒有發生變化,updateShouldNotify 應該返回false,但實際打印的結果:
flutter: updateShouldNotify:true flutter: A build flutter: F didChangeDependencies flutter: F build
實際返回了 true,由於先後 _userInfo 對象引用發生了變化,在 UserInfo 中重寫 ==,比較具體的name 和 age 是否相等:
@override bool operator ==(Object other) { if (!(other is UserInfo)) { return false; } var old = other as UserInfo; return name == old.name && age == old.age; }
再次運行,日誌以下:
flutter: updateShouldNotify:false flutter: A build flutter: F build
還有一種錯誤寫法:
class _InheritedWidgetDemoState extends State<InheritedWidgetDemo> { UserInfo _userInfo = UserInfo(name: '老孟', age: 18); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('InheritedWidget Demo'), ), body: Center( child: MyInheritedWidget( userInfo: _userInfo, child: A( child: F(), ), ), ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { _userInfo.name = '老孟1'; // _userInfo = UserInfo(name: '老孟', age: 18); }); }, ), ); } }
重點看這部分修改:
floatingActionButton: FloatingActionButton( onPressed: () { setState(() { _userInfo.name = '老孟1'; // _userInfo = UserInfo(name: '老孟', age: 18); }); }, ),
將 _userInfo = UserInfo(name: '老孟', age: 18) 修改成 _userInfo.name = '老孟1',猜猜 updateShouldNotify 返回是 true or false?
運行日誌:
flutter: updateShouldNotify:false flutter: A build flutter: F build
是否是感受很是難以想象,兩次的 name 值不同啊?
那是由於 _userInfo.name = '老孟1' 也修改了 oldWidget 的_userInfo,先後兩次都指向了同一個對象引用。
不少人應該會有這樣一個疑問,假設設置 updateShouldNotify 返回false,點擊的時候UI也會更改,由於整顆樹都 rebuild 了,那麼 updateShouldNotify 由什麼意義呢?
確定是有意義的,看以下場景,F 組件使用 InheritedWidget 的共享數據訪問服務器接口,獲取服務器數據並展現,若是 updateShouldNotify 返回 false,那麼 F 組件 rebuild 時只會執行 build 函數,而訪問服務器接口是一個耗時工做,考慮性能因素,不能將訪問服務器接口放在 build 函數中,那麼 InheritedWidget 數據的更新就沒法更新其依賴的組件,而 updateShouldNotify 返回 true時, F 組件 rebuild 時會執行 didChangeDependencies 和 build 函數,此時能夠將訪問服務器接口放在 didChangeDependencies 函數中,這也是 didChangeDependencies 生命週期存在的意義。
下面重點來了,那麼如何正確使用 InheritedWidget 組件,答案是 InheritedWidget + ValueNotifier,關於 **的用法能夠到個人我的博客中查看,地址:http://laomengit.com/flutter/widgets/ValueListenableBuilder.html ,這裏不詳細展開介紹。
修改 MyInheritedWidget 代碼:
class MyInheritedWidget extends InheritedWidget { ValueNotifier<UserInfo> _valueNotifier; ValueNotifier<UserInfo> get valueNotifier => _valueNotifier; MyInheritedWidget(UserInfo userInfo, {Widget child}) : super(child: child) { _valueNotifier = ValueNotifier<UserInfo>(userInfo); } static MyInheritedWidget of(BuildContext context) { return context.getElementForInheritedWidgetOfExactType<MyInheritedWidget>().widget; } void updateData(UserInfo info) { _valueNotifier.value = info; } @override bool updateShouldNotify(covariant MyInheritedWidget oldWidget) { return false; } }
主要的變化是:
靜態方法 of 由
static MyInheritedWidget of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>(); }
改成
static MyInheritedWidget of(BuildContext context) { return context.getElementForInheritedWidgetOfExactType<MyInheritedWidget>().widget; }
區別是 dependOnInheritedWidgetOfExactType 註冊了依賴關係,而 getElementForInheritedWidgetOfExactType 未註冊,後面的源碼部分會詳細分析。
修改 F 組件的代碼:
class F extends StatefulWidget { @override _FState createState() => _FState(); } class _FState extends State<F> { @override void initState() { super.initState(); print('F initState'); } @override void didChangeDependencies() { super.didChangeDependencies(); print('F didChangeDependencies'); } @override Widget build(BuildContext context) { print('F build'); return ValueListenableBuilder( builder: (context, UserInfo value, child) { return Text('${value.name}'); }, valueListenable: MyInheritedWidget.of(context).valueNotifier, ); } @override void dispose() { super.dispose(); print('F dispose'); } }
變化:
_InheritedWidgetDemoState 代碼修改以下:
@override Widget build(BuildContext context) { return MyInheritedWidget(UserInfo(name: '老孟', age: 18), child: Builder( builder: (context) { return Scaffold( appBar: AppBar( title: Text('InheritedWidget Demo'), ), body: Center( child: A( child: F(), ), ), floatingActionButton: FloatingActionButton( onPressed: () { MyInheritedWidget.of(context) .updateData(UserInfo(name: '老孟${_clickCount++}', age: 18)); }, ), ); }, )); }
運行效果:
重點看 rebuild 的組件,無關的組件(好比 A)沒有 rebuild。
固然也可使用 Provider 實現子組件更新,增長 UserInfoModel:
class UserInfoModel extends ChangeNotifier { UserInfoModel(this._userInfo); UserInfo _userInfo; UserInfo get userInfo => _userInfo; void update(UserInfo userInfo) { _userInfo = userInfo; notifyListeners(); } }
修改 _InheritedWidgetDemoState:
class _InheritedWidgetDemoState extends State<InheritedWidgetDemo> { int _clickCount =0; @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider( create: (_) => UserInfoModel(UserInfo(name: '老孟', age: 18))), ], builder: (context, child) { return Scaffold( appBar: AppBar( title: Text('InheritedWidget Demo'), ), body: Center( child: A( child: F(), ), ), floatingActionButton: Consumer<UserInfoModel>( builder: (ctx, userInfoModel, child) { return FloatingActionButton( child: child, onPressed: () { userInfoModel.update(UserInfo(name: '老孟${_clickCount++}', age: 18)); }, ); }, ), ); }, ); } }
分析源碼的時候必定要先想一想,若是是我來實現這個組件,要如何實現? InheritedWidget 組件主要實現了兩個功能:
依賴 InheritedWidget 的子組件如何獲取 InheritedWidget 組件的共享數據?首先查看獲取共享數據的方法:
context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
這段代碼獲取 MyInheritedWidget 實例,dependOnInheritedWidgetOfExactType 方法是 BuildContext 的方法,Element 實現了此方法:
@override T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) { assert(_debugCheckStateIsActiveForAncestorLookup()); final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T]; if (ancestor != null) { assert(ancestor is InheritedElement); return dependOnInheritedElement(ancestor, aspect: aspect) as T; } _hadUnsatisfiedDependencies = true; return null; }
從上面的源代碼能夠看出,首先到 _inheritedWidgets 中查找指定的 InheritedElement,_inheritedWidgets 這個 Map 是哪裏來的?何時被初始化的?看下 _inheritedWidgets 屬性的定義:
Map<Type, InheritedElement> _inheritedWidgets;
查找其引用和賦值的源代碼:
@override void _updateInheritance() { assert(_active); final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets; if (incomingWidgets != null) _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets); else _inheritedWidgets = HashMap<Type, InheritedElement>(); _inheritedWidgets[widget.runtimeType] = this; }
上面的代碼在 InheritedElement 中,但此方法在 Element 中也實現了:
void _updateInheritance() { assert(_active); _inheritedWidgets = _parent?._inheritedWidgets; }
上面的代碼說明了非 InheritedElement 的 Element 中 _inheritedWidgets 等於父組件的 _inheritedWidgets,而 InheritedElement 會將自身添加到 _inheritedWidgets 中,系統經過此方式將組件和 InheritedWidgets 的依賴關係層層向下傳遞,每個 Element 中都含有 _inheritedWidgets 集合,此集合中包含了此組件的父組件且是InheritedWidgets 組件的引用關係。
那麼是何時執行的 _updateInheritance 方法的呢?經過查找其引用,發如今 mount 和 activate 中調用了 _updateInheritance 方法。關於 mount 和 activate 階段能夠查看 Stateful 組件的生命週期 文章。
下面查看 dependOnInheritedElement 方法,在查找到依賴的 InheritedElement 後,執行 dependOnInheritedElement 方法,源代碼以下:
@override InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) { assert(ancestor != null); _dependencies ??= HashSet<InheritedElement>(); _dependencies.add(ancestor); ancestor.updateDependencies(this, aspect); return ancestor.widget; }
updateDependencies 方法源代碼以下:
@protected void updateDependencies(Element dependent, Object aspect) { setDependencies(dependent, null); } @protected void setDependencies(Element dependent, Object value) { _dependents[dependent] = value; }
上面的代碼就是向 _dependents 中添加註冊, InheritedWidget 組件更新時能夠更具此列表通知子組件。
再來看下的代碼:
@Deprecated( 'Use getElementForInheritedWidgetOfExactType instead. ' 'This feature was deprecated after v1.12.1.' ) @override InheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType) { assert(_debugCheckStateIsActiveForAncestorLookup()); final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType]; return ancestor; } @override InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() { assert(_debugCheckStateIsActiveForAncestorLookup()); final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T]; return ancestor; }
在 v1.12.1 版本之前使用 ancestorInheritedElementForWidgetOfExactType 方法,如今使用 getElementForInheritedWidgetOfExactType 方法,此方法和 dependOnInheritedWidgetOfExactType 方法的惟一卻不就是 getElementForInheritedWidgetOfExactType 方法沒有註冊,也就是使用 getElementForInheritedWidgetOfExactType 方法獲取共享數據的子組件,不會在 InheritedWidget 組件重建時調用 didChangeDependencies 方法。
下面看看爲何 InheritedWidget 組件數據方式變化,重建時會調用其 didChangeDependencies 方法?
當組件發生變化時會調用 update方法:
@override void update(ProxyWidget newWidget) { final ProxyWidget oldWidget = widget; assert(widget != null); assert(widget != newWidget); super.update(newWidget); assert(widget == newWidget); updated(oldWidget); _dirty = true; rebuild(); }
InheritedElement 重寫了 updated 方法:
@override void updated(InheritedWidget oldWidget) { if (widget.updateShouldNotify(oldWidget)) super.updated(oldWidget); }
當 updateShouldNotify 返回 true時,執行更新操做。而其父類的 updated 方法以下:
@protected void updated(covariant ProxyWidget oldWidget) { notifyClients(oldWidget); }
notifyClients 方法源代碼:
@override void notifyClients(InheritedWidget oldWidget) { assert(_debugCheckOwnerBuildTargetExists('notifyClients')); for (final Element dependent in _dependents.keys) { assert(() { // check that it really is our descendant Element ancestor = dependent._parent; while (ancestor != this && ancestor != null) ancestor = ancestor._parent; return ancestor == this; }()); // check that it really depends on us assert(dependent._dependencies.contains(this)); notifyDependent(oldWidget, dependent); } }
遍歷 _dependents,上面已經介紹,_dependents 是依賴它的子組件集合,遍歷調用 notifyDependent 方法:
@protected void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) { dependent.didChangeDependencies(); }
這裏調用了 didChangeDependencies 方法,這也是 InheritedWidget 組件發生變化,重建時執行生命週期 didChangeDependencies。
上面的代碼都是在 InheritedElement 中的,在看下 InheritedWidget 的源代碼:
abstract class InheritedWidget extends ProxyWidget { const InheritedWidget({ Key key, Widget child }) : super(key: key, child: child); @override InheritedElement createElement() => InheritedElement(this); @protected bool updateShouldNotify(covariant InheritedWidget oldWidget); }
這個類很是簡單,建立了一個 InheritedElement,定義了一個 updateShouldNotify 方法(上面已經詳細介紹此方法的做用),子類須要重寫。
經過上面的源碼解析,子組件獲取共享數據時,實際是直接在 _inheritedWidgets 集合中匹配的,經過斷點也能夠查看其中的內容:
經過上面的分析,InheritedWidget 組件流程以下:
說明:
那麼爲何是在當前組件的中保存這樣一個 Map 集合,而不是依次向上查找呢(我最開始的想法)?
下面是我我的的一點見解,若是你有不一樣的見解,歡迎一塊兒討論:
當前組件的中保存這樣一個 Map 集合,獲取共享數據時直接定位依賴的 InheritedWidget,複雜度 O(1) 。
而依次向上查找的複雜度是 O(n),樹的結構越深,消耗時間越長,複雜度線性增加。
老孟Flutter博客(330個控件用法+實戰入門系列文章):http://laomengit.com
添加微信或者公衆號領取 《330個控件大全》和 《Flutter 實戰》PDF。
歡迎加入Flutter交流羣(微信:laomengit)、關注公衆號【老孟Flutter】:
![]() |
![]() |