
背景
本文对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界面开发(闲鱼)