阅读 363

Flutter开发日记-数据传递/状态管理的方式和应用

背景

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