Flutter頁面傳值的幾種方式
今天來聊聊Flutter頁面傳值的幾種方式:
- InheritWidget
- Notification
- Eventbus
(當前Flutter版本:2.0.4)
InheritWidget
如果看過Provider的源碼的同學都知道,Provider跨組件傳值的原理就是根據系統提供的InheritWidget實現的,讓我們來看一下這個組件。
InheritWidget是一個抽象類,我們寫一個保存用戶信息的類UserInfoInheritWidget繼承於InheritWidget:
class UserInfoInheritWidget extends InheritedWidget { UserInfoBean userInfoBean; UserInfoInheritWidget({Key key, this.userInfoBean, Widget child}) : super (child: child); static UserInfoWidget of(BuildContext context){ return context.dependOnInheritedWidgetOfExactType<UserInfoWidget>(); } @override bool updateShouldNotify(UserInfoInheritWidget oldWidget) { return oldWidget.userInfoBean != userInfoBean; } }
我們在這裡面定義瞭一個靜態方法:of,並且傳入瞭一個context,根據context獲取當前類,拿到當前類中的UserInfoBean,其實獲取主題數據也是根據InheritWidget這種方式獲取Theme.of(context),關於of方法後面重點講一下,updateShouldNotify是刷新機制,什麼時候刷新數據
還有一個用戶信息的實體:
class UserInfoBean { String name; String address; UserInfoBean({this.name, this.address}); }
我們做兩個頁面,第一個頁面顯示用戶信息,還有一個按鈕,點擊按鈕跳轉到第二個頁面,同樣也是顯示用戶信息:
class Page19PassByValue extends StatefulWidget { @override _Page19PassByValueState createState() => _Page19PassByValueState(); } class _Page19PassByValueState extends State<Page19PassByValue> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('PassByValue'), ), body: DefaultTextStyle( style: TextStyle(fontSize: 30, color: Colors.black), child: Column( children: [ Text(UserInfoWidget.of(context)!.userInfoBean.name), Text(UserInfoWidget.of(context)!.userInfoBean.address), SizedBox(height: 40), TextButton( child: Text('點擊跳轉'), onPressed: (){ Navigator.of(context).push(CupertinoPageRoute(builder: (context){ return DetailPage(); })); }, ) ], ), ), ); } }
class DetailPage extends StatefulWidget { @override _DetailPageState createState() => _DetailPageState(); } class _DetailPageState extends State<DetailPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Detail'), ), body: DefaultTextStyle( style: TextStyle(fontSize: 30, color: Colors.black), child: Center( child: Column( children: [ Text(UserInfoWidget.of(context).userInfoBean.name), Text(UserInfoWidget.of(context).userInfoBean.address), TextButton( onPressed: () { setState(() { UserInfoWidget.of(context)!.updateBean('wf123','address123'); }); }, child: Text('點擊修改')) ], ), ), ) ); } }
由於我們這裡是跨組件傳值,需要把UserInfoWidget放在MaterialApp的上層,並給UserInfoBean一個初始值:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return UserInfoWidget( userInfoBean: UserInfoBean(name: 'wf', address: 'address'), child: MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ), ); } }
這樣就實現瞭一個跨組件傳值,但是還有個問題,我們給UserInfoWidget賦值的時候是在最頂層,在真實業務場景中,如果我們把UserInfo的賦值放在MaterialApp上面,這時候我們還沒拿到用戶數據呢,所以就要有一個可以更新UserInfo的方法,並且修改後立即刷新,我們可以借助setState,把我們上面定義的UserInfoWidget改個名字然後封裝在StatefulWidget 中:
class _UserInfoInheritWidget extends InheritedWidget { UserInfoBean userInfoBean; Function update; _UserInfoInheritWidget({Key key, this.userInfoBean, this.update, Widget child}) : super (child: child); updateBean(String name, String address){ update(name, address); } @override bool updateShouldNotify(_UserInfoInheritWidget oldWidget) { return oldWidget.userInfoBean != userInfoBean; } } class UserInfoWidget extends StatefulWidget { UserInfoBean userInfoBean; Widget child; UserInfoWidget({Key key, this.userInfoBean, this.child}) : super (key: key); static _UserInfoInheritWidget of(BuildContext context){ return context.dependOnInheritedWidgetOfExactType<_UserInfoInheritWidget>(); } @override State<StatefulWidget> createState() => _UserInfoState(); } class _UserInfoState extends State <UserInfoWidget> { _update(String name, String address){ UserInfoBean bean = UserInfoBean(name: name, address: address); widget.userInfoBean = bean; setState(() {}); } @override Widget build(BuildContext context) { return _UserInfoInheritWidget( child: widget.child, userInfoBean: widget.userInfoBean, update: _update, ); } }
上面把繼承自InheritWidget的類改瞭一個名字:_UserInfoInheritWidget,對外隻暴露用StatefulWidget封裝過的UserInfoWidget,向_UserInfoInheritWidget傳入瞭包含setState的更新數據方法,更新數據的時候通過UserInfoWidget.of(context)獲取到繼承於InheritWidget的_UserInfoInheritWidget類,調用updateBean方法實際上就調用瞭包含setState的方法,所以做到瞭數據更新和頁面刷新
下面重點說一下UserInfoWidget.of(context)是如何獲取到繼承於InheritWidget類的對象的,通過查看類似的方法:Theme.of(context)發現是根據dependOnInheritedWidgetOfExactType,於是我們也照著它的樣子獲取到瞭_UserInfoInheritWidget,點到dependOnInheritedWidgetOfExactType源碼中看一下,發現跳轉到瞭BuildContext中定義瞭這個方法:
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });
瞭解Widget、Element、RenderObject三隻之間關系的同學都知道,其實context是Element的一個實例,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是從哪來的,我們搜索一下在Element中發現
void _updateInheritance() { assert(_lifecycleState == _ElementLifecycle.active); _inheritedWidgets = _parent?._inheritedWidgets; }
再看一下_updateInheritance方法是什麼時候調用的
@mustCallSuper void mount(Element? parent, dynamic newSlot) { ... ...省略無關代碼 _parent = parent; _slot = newSlot; _lifecycleState = _ElementLifecycle.active; _depth = _parent != null ? _parent!.depth + 1 : 1; if (parent != null) // Only assign ownership if the parent is non-null _owner = parent.owner; final Key? key = widget.key; if (key is GlobalKey) { key._register(this); } _updateInheritance();//這裡調用瞭一次 }
還有:
@mustCallSuper void activate() { ... ...已省略無關代碼 final bool hadDependencies = (_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfiedDependencies; _lifecycleState = _ElementLifecycle.active; _dependencies?.clear(); _hadUnsatisfiedDependencies = false; _updateInheritance();//這裡又調用瞭一次 if (_dirty) owner!.scheduleBuildFor(this); if (hadDependencies) didChangeDependencies(); }
從上面代碼我們可以看到每個頁面的Element都會通過_parent向下級傳遞父級信息,而我們的UserInfoWidget就保存在_parent中的_inheritedWidgets集合中:Map<Type, InheritedElement>? _inheritedWidgets;,當_inheritedWidgets在頁面樹中向下傳遞的時候,如果當前Widget是InheritWidget,在當前Widget對應的Element中先看_parent傳過來的_inheritedWidgets是否為空,如果為空就新建一個集合,把自己存到這個集合中,以當前的類型作為key(這也是為什麼調用of方法中的context.dependOnInheritedWidgetOfExactType方法為什麼要傳當前類型的原因),從_inheritedWidgets集合中去取值;如果不為空直接把自己存進去,這就是of的原理瞭。
Notification
上面講的InheritWidget一般是根部組建向子級組件傳值,Notification是從子級組件向父級組件傳值,下面我們來看一下它的用法
class Page19PassByValue extends StatefulWidget { @override _Page19PassByValueState createState() => _Page19PassByValueState(); } class _Page19PassByValueState extends State<Page19PassByValue> { UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address'); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('PassByValue'), ), body: Center( child: NotificationListener<MyNotification>( onNotification: (MyNotification data) { userInfoBean = data.userInfoBean; setState(() {}); ///這裡需要返回一個bool值,true表示阻止事件繼續向上傳遞,false表示事件可以繼續向上傳遞到父級組件 return true; }, child: Builder( ///這裡用瞭一個Builder包裝瞭一下,為的是能取到 ///NotificationListener的context builder: (context) { return Column( children: [ Text(userInfoBean.name), Text(userInfoBean.address), Container( child: FlatButton( child: Text('點擊傳值'), onPressed: () { MyNotification(userInfoBean: UserInfoBean(name: 'wf123', address: 'address123')).dispatch(context); }, ), ) ], ); }, ), ), ), ); } }
///Notification是一個抽象類, ///使用Notification需要自定義一個class繼承Notification class MyNotification extends Notification { UserInfoBean userInfoBean; MyNotification({this.userInfoBean}) : super(); }
我們到源碼中看一下這個dispatch方法:
void dispatch(BuildContext target) { // The `target` may be null if the subtree the notification is supposed to be // dispatched in is in the process of being disposed. target?.visitAncestorElements(visitAncestor); }
target就是我們傳進來的context,也就是調用瞭BuildContext的visitAncestorElements方法,並且把visitAncestor方法作為一個參數傳過去,visitAncestor方法返回一個bool值:
@protected @mustCallSuper bool visitAncestor(Element element) { if (element is StatelessElement) { final StatelessWidget widget = element.widget; if (widget is NotificationListener<Notification>) { if (widget._dispatch(this, element)) // that function checks the type dynamically return false; } } return true; }
我們進入Element內部看一下visitAncestorElements方法的實現:
@override void visitAncestorElements(bool visitor(Element element)) { assert(_debugCheckStateIsActiveForAncestorLookup()); Element? ancestor = _parent; while (ancestor != null && visitor(ancestor)) ancestor = ancestor._parent; }
當有父級節點,並且visitor方法返回true的時候執行while循環,visitor是Notification類傳進來的方法,回過頭再看visitor方法的實現,當Element向visitor方法傳遞的ancestor是NotificationListener類的情況下,再判斷widget._dispatch方法,而widget._dispatch方法:
final NotificationListenerCallback<T>? onNotification; bool _dispatch(Notification notification, Element element) { if (onNotification != null && notification is T) { final bool result = onNotification!(notification); return result == true; // so that null and false have the same effect } return false; }
就是我們在外面寫的onNotification方法的實現,我們在外面實現的onNotification方法返回true(即阻止事件繼續向上傳遞),上面的while循環主要是為瞭執行我們onNotification裡面的方法.
總結一下:MyNotification執行dispatch方法,傳遞context,根據當前context向父級查找對應NotificationListener,並且執行NotificationListener裡面的onNotification方法,返回true,則事件不再向上級傳遞,如果返回false則事件繼續向上一個NotificationListener傳遞,並執行裡面對應的方法。Notification主要用在同一個頁面中,子級向父級傳值,比較輕量級,不過如果我們用瞭Provider可能就就直接借助Provider傳值瞭。
Eventbus
Eventbus用於兩個不同的頁面,可以跨多級頁面傳值,用法也比較簡單,我創建瞭一個EventBusUtil來創建一個單例
import ‘package:event_bus/event_bus.dart’;
class EventBusUtil { static EventBus ? _instance; static EventBus getInstance(){ if (_instance == null) { _instance = EventBus(); } return _instance!; } }
在第一個頁面監聽:
class Page19PassByValue extends StatefulWidget { @override _Page19PassByValueState createState() => _Page19PassByValueState(); } class _Page19PassByValueState extends State<Page19PassByValue> { UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address'); @override void initState() { super.initState(); EventBusUtil.getInstance().on<UserInfoBean>().listen((event) { setState(() { userInfoBean = event; }); }); } @override void dispose() { super.dispose(); //不用的時候記得關閉 EventBusUtil.getInstance().destroy(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('PassByValue'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(userInfoBean.name), Text(userInfoBean.address), TextButton(onPressed: (){ Navigator.of(context).push(CupertinoPageRoute(builder: (_){ return EventBusDetailPage(); })); }, child: Text('點擊跳轉')) ], ), ), ); } }
在第二個頁面發送事件:
class EventBusDetailPage extends StatefulWidget { @override _EventBusDetailPageState createState() => _EventBusDetailPageState(); } class _EventBusDetailPageState extends State<EventBusDetailPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('EventBusDetail'), ), body: Center( child: TextButton(onPressed: (){ EventBusUtil.getInstance().fire(UserInfoBean(name: 'name EventBus', address: 'address EventBus')); }, child: Text('點擊傳值')), ), ); } }
我們看一下EventBus的源碼,發現隻有幾十行代碼,他的內部是創建瞭一個StreamController,通過StreamController來實現跨組件傳值,我們也可以直接使用一下這個StreamController實現頁面傳值:
class Page19PassByValue extends StatefulWidget { @override _Page19PassByValueState createState() => _Page19PassByValueState(); } StreamController controller = StreamController(); class _Page19PassByValueState extends State<Page19PassByValue> { //設置一個初始值 UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address'); @override void initState() { super.initState(); controller.stream.listen((event) { setState(() { userInfoBean = event; }); }); } @override void dispose() { super.dispose(); //頁面銷毀的時候記得關閉 controller.close(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('PassByValue'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(userInfoBean.name), Text(userInfoBean.address), TextButton(onPressed: (){ Navigator.of(context).push(CupertinoPageRoute(builder: (_){ return MyStreamControllerDetail(); })); }, child: Text('點擊跳轉')) ], ), ) ); } }
class MyStreamControllerDetail extends StatefulWidget { @override State<StatefulWidget> createState() { return _MyStreamControllerDetailState(); } } class _MyStreamControllerDetailState extends State <MyStreamControllerDetail> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('StreamController'), ), body: Center( child: TextButton(onPressed: (){ //返回上個頁面,會發現頁面的數據已經變瞭 controller.sink.add(UserInfoBean(name: 'StreamController pass name: 123', address: 'StreamController pass address 123')); }, child: Text('點擊傳值'),), ), ); } }
到此這篇關於Flutter頁面傳值的幾種方式的文章就介紹到這瞭,更多相關Flutter頁面傳值內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Flutter有狀態組件使用詳解
- flutter狀態管理Provider的使用學習
- Flutter 如何正確顯示SnackBar
- Flutter之PageView頁面緩存與KeepAlive
- flutter實現appbar下選項卡切換