背景
本文對Flutter常見的資料共享方式進行了總結,方便今後開發中的使用和補充。
部分demo為搬運的例子。
屬性傳值
特點:同一元件樹 逐層傳遞
實現:通過構造器傳遞資料
class DataTransferByConstructorPage extends StatelessWidget {
final TransferDataEntity data;
DataTransferByConstructorPage({this.data});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("構造器方式"),
),
body: Text('test')
);
}
}
// 父元件通過構造器傳遞data屬性
class ParentWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
TransferDataEntity data = new TransferDataEntity()
return DataTransferByConstructorPage(data)
}
}
複製程式碼
問題:
當我們需要跨層傳遞時 就需要逐層給子元件傳入引數,
當 資料量增加或者 樹的深度增加時
實現起來就十分麻煩
這種需要跨層傳遞的場景,我們就可以使用 InheritedWidget
// 盜一張官方的圖
InheritedWidget
特點:同一元件樹傳遞資料 從上到下 跨層傳遞,是flutter官方提供的功能型元件
找了個demo---只有讀功能:
class CountContainer extends InheritedWidget {
// 方便其子 Widget 在 Widget 樹中找到它
static CountContainer of(BuildContext context) => context.inheritFromWidgetOfExactType(CountContainer) as CountContainer;
final int count;
CountContainer({
Key key,
@required this.count,
@required Widget child,
}): super(key: key, child: child);
// 判斷是否需要更新
@override
bool updateShouldNotify(CountContainer oldWidget) => count != oldWidget.count;
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
// 將 CountContainer 作為根節點,並使用 0 作為初始化 count
return CountContainer(
count: 0,
child: Counter()
);
}
}
class Counter extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 獲取 InheritedWidget 節點
CountContainer state = CountContainer.of(context);
return Scaffold(
appBar: AppBar(title: Text("InheritedWidget demo")),
body: Text(
'You have pushed the button this many times: ${state.count}',
),
);
}
複製程式碼
1 CountContainer繼承自InheritedWidget,CountContainer 宣告瞭一個final屬性和of方法,of方法返回CountContainer物件,方便子widget在widget找到它並且獲取屬性值。
2 重寫了 updateShouldNotify 方法,當count修改時 通知繼承他的widget更新
3 在我們的頁面中 將我們的檢視widget作為child傳遞給CountContainer,並使用靜態方法of獲取到當前上下文的CountContainer,以此讀取他的屬性
找了另外一個demo---寫功能:
class CounterPage extends StatefulWidget {
CounterPage({Key key}) : super(key: key);
@override
_CounterPageState createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int count = 0;
void _incrementCounter() => setState(() {count++;});
@override
Widget build(BuildContext context) {
return CountContainer(
//increment: _incrementCounter,
model: this,
increment: _incrementCounter,
child:Counter()
);
}
}
class CountContainer extends InheritedWidget {
static CountContainer of(BuildContext context) => context.inheritFromWidgetOfExactType(CountContainer) as CountContainer;
final _CounterPageState model;
final Function() increment;
CountContainer({
Key key,
@required this.model,
@required this.increment,
@required Widget child,
}): super(key: key, child: child);
@override
bool updateShouldNotify(CountContainer oldWidget) => model != oldWidget.model;
}
複製程式碼
1 將資料本身和操作他的方法宣告放在檢視元件,inheritedwidget只保留對它的引用,利用傳入的_incrementCounter,操作count屬性,當modal上面的屬性發生變化,即觸發繼承它的widget的更新
2 此方法需要將方法和屬性全部定義在widget樹的最上層。然後一起傳入給inheritedwidget,思想有些類似於之前實現相簿plugin使用的控制器。
控制器儲存了所有操作,widget共享的資料(相簿列表,當前所在相簿 等)的操作方法。我們向下傳遞一個controller物件,子widget需要讀或者寫資料時,通過控制器方法獲取。
但這種方法會造成每次更新一個資料,就會造成整棵樹的rebuild.
Inherited widgets, when referenced in this way, will cause the consumer to rebuild when the inherited widget itself changes state
如果考慮使用InheritedWidget實現這個功能,可以降低資料更新成本
基於InheritedWidget實現的第三方庫provider,瞭解一下~
provider
特點: 做資料讀寫共享
又雙叒叕引用了一個demo:
// 定義資料
//定義需要共享的資料模型,通過混入ChangeNotifier管理聽眾
class DataModel with ChangeNotifier {
int data = 0;
//讀方法
int get data => _data;
//寫方法
void increment() {
_data = _data*2;
notifyListeners();
}
}
// 將最上層widget包裹在provider內
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//通過Provider元件封裝資料資源
return ChangeNotifierProvider.value(
value: DataModel(),//需要共享的資料資源
child: MaterialApp(
home: MyPage(),
)
);
}
}
// 下級widget中獲取或運算元據,同一樹的其他widget會重新觸發build
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
//取出資源
final _counter = Provider.of<DataModel>(context);
return Scaffold(
//展示資源中的資料
body: Text('Counter: ${_data.data}'),
//用資源更新方法來設定按鈕點選回撥
floatingActionButton:FloatingActionButton(
onPressed: _data.increment,
child: Icon(Icons.add),
));
}
}
複製程式碼
1 可以看出使用方式類似 InheritedWidget,將整棵樹包裹在provider裡,實現資料的跨層傳遞
2 provider可實現更小粒度的更新,當頁面中某部分不依賴於provider資料,放在consumer的child屬性中,而每次資料更新,只重新執行builder
Their optional child argument allows to rebuild only a very specific part of the widget tree
In this example, only Bar will rebuild when A updates. Foo and Baz won't unnecesseraly rebuild.
Foo(
child: Consumer<A>(
builder: (_, a, child) {
return Bar(a: a, child: child);
},
child: Baz(),
),
)
複製程式碼
問題:InheritedWidget和peovider提供了父->子的資料流機制,但如果此時我們需要子元件主動的分發事件和資料呢,那就闊以用到Notification了。
Notification
通知(Notification)是Flutter中一個重要的機制,在widget樹中,每一個節點都可以分發通知,通知會沿著當前節點向上傳遞,所有父節點都可以通過NotificationListener來監聽通知。Flutter中將這種由子向父的傳遞通知的機制稱為通知冒泡(Notification Bubbling)。通知冒泡和使用者觸控事件冒泡是相似的,但有一點不同:通知冒泡可以中止,但使用者觸控事件不行。 特點:同一元件樹傳遞資料 從下到上 通知事件 通知接收方可在事件物件中獲取資料 實現:
特點: 同一元件樹,子到父分發通知觸發事件,可跨層。 又找了個demo:
class CustomNotification extends Notification {
CustomNotification(this.msg);
final String msg;
}
//抽離出一個子Widget用來發通知
class CustomChild extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
//按鈕點選時分發通知
onPressed: () => CustomNotification("Hi").dispatch(context),
child: Text("Fire Notification"),
);
}
}
class _MyHomePageState extends State<MyHomePage> {
String _msg = "通知:";
@override
Widget build(BuildContext context) {
//監聽通知
return NotificationListener<CustomNotification>(
onNotification: (notification) {
setState(() {_msg += notification.msg+" ";});//收到子Widget通知,更新msg
},
child:Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Text(_msg),CustomChild()],//將子Widget加入到檢視樹中
)
);
}
}
複製程式碼
1 宣告一個整合自Notification的子類CustomNotification
2 CustomChild 子元件例項化 CustomNotification並通過dispatch 觸發沿著element樹的向上冒泡通知
3 _MyHomePageState 通過NotificationListener 監聽 指定型別的CustomNotification 型別通知,並在onNotification中獲取到通知的例項物件和資料。
問題:上述提到的三種方法都依賴於widget樹,適用於widget之間有父子關係的場景,當我們需要跨頁面傳遞資料時,可以考慮使用event_bus .(需要安裝第三方依賴)
eventBus
特點:資料傳遞 ,不限制於同一元件樹,使用釋出/訂閱者模式實現跨元件資料通訊
又找了個demo:
---------- pubspec.yaml
dependencies:
event_bus: 1.1.0
---------- 自定義事件
class CustomEvent {
String msg;
CustomEvent(this.msg);
}
---------- 監聽事件
//建立公共的event bus
EventBus eventBus = new EventBus();
//第一個頁面
initState() {
//監聽CustomEvent事件,重新整理UI
subscription = eventBus.on<CustomEvent>().listen((event) {
setState(() {msg+= event.msg;});//更新msg
});
super.initState();
}
---------- 觸發事件
()=> eventBus.fire(CustomEvent("hello"))
---------- 取消訂閱事件(否則會在元件銷燬後發生記憶體洩露)
subscription.cancel();//State銷燬時,清理註冊
---------- 全部程式碼
class CustomEvent {
String msg;
CustomEvent(this.msg);
}
EventBus eventBus = new EventBus();
class FirstPage extends StatefulWidget {
@override
State<StatefulWidget> createState()=>_FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
String msg = "通知:";
StreamSubscription subscription;
@override
void initState() {
//監聽CustomEvent事件,重新整理UI
subscription = eventBus.on<CustomEvent>().listen((event) {
print(event);
setState(() {
msg += event.msg;
});
});
super.initState();
}
dispose() {
subscription.cancel();//State銷燬時,清理註冊
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("First Page"),),
body:Text(msg),
floatingActionButton: FloatingActionButton(onPressed: ()=>Navigator.push(context,MaterialPageRoute(builder: (context) => SecondPage()))),
);
}
}
class SecondPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Second Page"),),
body: RaisedButton(
child: Text('Fire Event'),
// 觸發CustomEvent事件
onPressed: ()=> eventBus.fire(CustomEvent("hello"))
),
);
}
}
複製程式碼
通過一個資料狀態,可以有多個訂閱者,實現批量的資料同步。
歡迎補充 歡迎指正~
參考文章
time.geekbang.org/column/arti… 極客時間
zhuanlan.zhihu.com/p/36577285 深入瞭解Flutter介面開發(閒魚)