Flutter | 数据共享

2022-02-11 14:31:09 浏览数 (1)

本文示例代码

数据共享 InheritedWidget

InheritedWidget 是 Flutter 中非常重要的一个功能型组件,它提供了一种数据在 widget 树中从上到下传递的方式。例如在根 Widget 中通过 InheritedWidget 共享了一个数据,那么我们就可以在任意的子 Widget 中获取改共享的数据;

这个特性在一些需要 widget 树中共享数据的场景非常方便,如 Fluter SDK 正是通过该 Widget 来共享应用主题 和 Locale(语言环境)信息的;

didChangeDependencies

该回调是 State 对象的,他会在依赖发生变化时被 Flutter Framework 调用,这个依赖指的就是 widget 是否使用了父 widget 中的 InheritedWidget 的数据;

如使用了,则代表该组件依赖 InheritedWidget,如果没有使用,则代表没有依赖。

这种机制可以使子组件所依赖的 InheritedWidget 在变化时来更新自身,例如主题,等发生变化的时候,依赖的子 widget 的 didChangeDependencies 方法就会被调用

下面看一个栗子:

代码语言:javascript复制
class ShareDataWidget extends InheritedWidget {
  //需要共享的数据
  final int data;

  ShareDataWidget({@required this.data, Widget child}) : super(child: child);

  //定义一个便捷的方法,方便子树中的 widget获取共享数据
  static ShareDataWidget of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType();
  }

  ///该回调决定当 data 发生变化的时候,是否通知子树中依赖 data 的 widget
  @override
  bool updateShouldNotify(covariant ShareDataWidget oldWidget) {
    //返回true:则子树中依赖当前 widget 的 widget 的 didChangeDependencies 会被调用
    return oldWidget.data != data;
  }
}
复制代码

上面定义了一个共享的 ShareDataWidget ,它继承自 InheritedWidget,保存了一个 data 属性,data 属性就是需要共享的数据

代码语言:javascript复制
class TestShareWidget extends StatefulWidget {
  @override
  _TestShareWidgetState createState() => _TestShareWidgetState();
}

class _TestShareWidgetState extends State<TestShareWidget> {
  @override
  Widget build(BuildContext context) {
    return Text(ShareDataWidget.of(context).data.toString());
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('发生改变');
  }
}
复制代码

上面实现了一个子组件,在 build 方法中使用了 ShareDataWidget 的数据,同时在回调中打印了日志

最后,创建一个按钮,点击一次,就让 ShareDataWidget 的值自增

代码语言:javascript复制
class TestInheritedWidget extends StatefulWidget {
  @override
  _TestInheritedWidgetState createState() => _TestInheritedWidgetState();
}

class _TestInheritedWidgetState extends State<TestInheritedWidget> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ShareDataWidget(
        data: count,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Padding(
              padding: const EdgeInsets.only(bottom: 20.0),
              child: TestShareWidget(),
            ),
            RaisedButton(
              child: Text("Increment"),
              //每点击一次,count 自增,然后重新 build,ShareDataWidget 将被更新
              onPressed: () => setState(() =>   count),
            )
          ],
        ),
      ),
    );
  }
}
复制代码

效果如下:

可见,依赖发生变化之后,子组件的 did... 方法就会被调用。需要注意的是如果 TestShareWidget 的 build 方法中没有使用 ShareDataWidget 的数据,那么他的 did... 方法将不会调用,因为他并没有依赖 ShareDataWidget ;

例如改成下面这样:

代码语言:javascript复制
class _TestShareWidgetState extends State<TestShareWidget> {
  @override
  Widget build(BuildContext context) {
    // return Text(ShareDataWidget.of(context).data.toString());
    return Text("test");
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('发生改变');
  }
}
复制代码

将 buid 方法中 依赖 ShareDataWidget 的代码注释掉之后,返回了一个固定的 Text,这样依赖,虽然 data 发生了变化,但是由于并没有依赖关系,所以 didChangeDependencies 方法不会被调用

因为数据发生变化时只对是该该数据的 Widget 更新是合理并且性能友好的

应该在 did.... 方法中做什么

一般来说,子 widget 会很少重新此方法,应为在依赖发生改变之后也会调用 build 方法。但是如果你需要在依赖发生改变的时候做一些昂贵的操作,如网络请求等,这时最好的方式就是在此方法中执行,这样可以避免每次在 build 的时候都执行这些昂贵的操作

深入理解 InheritedWidget 方法

如果我们只想要依赖数据,并不想在依赖变化时执行 didChangeDependencies 方法应该怎么搞,如下:

代码语言:javascript复制
//定义一个便捷的方法,方便子树中的 widget获取共享数据
static ShareDataWidget of(BuildContext context) {
  // return context.dependOnInheritedWidgetOfExactType();
    return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;
}
复制代码

将获取 ShareDataWidget 的方式改为 context.getElementForInheritedWidgetOfExactType().widget 即可

那么这两种方式的区别是啥呢,我们看一下源码:

代码语言:javascript复制
@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
  return ancestor;
}
复制代码
代码语言:javascript复制
@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;
}
复制代码

可以看到,dependOnInheritedWidgetOfExactType 比 getElementForInheritedWidgetOfExactType 多掉了 dependOnInheritedElement 方法,他的源码如下:

代码语言:javascript复制
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
  assert(ancestor != null);
  _dependencies ??= HashSet<InheritedElement>();
  _dependencies!.add(ancestor);
  ancestor.updateDependencies(this, aspect);
  return ancestor.widget;
}
复制代码

可以看到,dependOnInheritedElement 方法中主要是注册了依赖关系,所以,在调用 getElementForInheritedWidgetOfExactType 时,InheritedWidget 和依赖他的子孙组件便完成了注册,之后当 InheritedWidget 发生变化的时候,才可以更新依赖他的子孙组件,也就是 子孙组件的 build 和 didChangeDependencies 方法。

由于 getElementForInheritedWidgetOfExactType 没有注册依赖,所以当 InheritedElement 发生变化时,就不会更新相应子孙的 widget

注意:上例中,将 of 方法中改为 getElementForInheritedWidgetOfExactType 之后,_TestShareWidgetState 的 didChangeDependencies 方法确实不会被调用,但是build 方法还是调用了; 这是应为在点击按钮之后,会调用 _TestInheritedWidgetState的 setState 方法,此时页面会重新构建,就会导致 TestShareWidget() 也重新构建,所以他的 build 也会执行

在这种情况下,所以依赖 ShareDataWidget 的组件,只要调用了 _TestInheritedWidgetState 的 setState 方法,都会被重新执行 build,这是没必要的,那有什么办法可以避免呢,答案是使用缓存;

一个简单的做法就是通过封装一个 StatefulWidget ,将 Widget 树缓存起来,这样就可以放在 build 被执行;

跨组件状态共享 Provider

Flutter 中,状态管理一般的原则是:

  • 如果组件是私有的,则组件自己管理状态
  • 如果要跨组件共享,则状态由共同的父组件来管理

对于跨组件共享状态,管理的方式有很多中,如使用全局的实践总线 EventBus,他是一个观察者模式的实现,通过它就可以实现跨组件的状态同步:状态持有方:进行状态更新,发布状态和使用的;状态使用方(观察者) ,监听状态的改变事件来完成一些操作;

但是,通过观察者模式来实现跨组件有一些明显的缺点:

  • 必须显式的定义各种事件,不方便管理
  • 订阅者必须显式的注册状态改变回调,也必须在组件销毁的时候手动解绑回调,以避免内存泄漏

那有没有更好的管理方式呢,答案是肯定的,就是使用 InheritedWidget ,他的天生特性就是能绑定 InheritedWidget 与依赖他的子孙组件的依赖关系,并且当数据发生变化时,可以自动依赖子孙组件!,利用这个特性,我们可以将需要跨组件的状态保存在 InheritedWidget 中,然后在子组件中引用 InheritedWidget 即可。Flutter 社区著名的 Provider 包正是基于这个思想实现的一套跨组件状态共享的解决方案,下面我们便详细看一下 Provider 的用法和原理。

Provider

我们根据上面学习的 InheritedWidget 实现的思路来一步一步的实现一个最小功能的 Provider

定义一个需要保存数据的 InheritedWidget

代码语言:javascript复制
///一个通用的 InheritedProvider,保存任何需要跨组件共享的状态
class InheritedProvider<T> extends InheritedWidget {
  ///共享的状态使用泛型
  final T data;

  InheritedProvider({@required this.data, Widget child})
      : super(child: child);

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    ///返回true,表示每次都会调用子孙节点的 didChangeDependencies
    return true;
  }
}
复制代码

由于具体业务数据类型不可预期,为了通用性,这里使用了泛型

现在保存数据的地方有了,接下来需要做的就是在数据变化时重新构建 InheritedProvider,那么现在面临两个问题:

  1. 数据发生变化如何通知?
  2. 谁来重新构建 InheritedProvider

第一个问题其实很好解决,我们可以使用 EventBus 来进行通知,但是为了更贴近 Flutter 开发,我们使用 Flutter SDK 中提供的 ChangeNotifier 类,他继承自 Listenable ,也实现了一个 Flutter 风格的订阅者模式,定义大致如下:

代码语言:javascript复制
class ChangeNotifier implements Listenable {
  List listeners=[];
  @override
  void addListener(VoidCallback listener) {
     //添加监听器
     listeners.add(listener);
  }
  @override
  void removeListener(VoidCallback listener) {
    //移除监听器
    listeners.remove(listener);
  }
  
  void notifyListeners() {
    //通知所有监听器,触发监听器回调 
    listeners.forEach((item)=>item());
  }
   
  ... //省略无关代码
}
复制代码

我们可以使用 add ,remove 来添加,移除监听器,通过 notifyListeners 可以触发所有监听器的回调

接着我们将需要共享的状态放在一个 Model 类中,然后继承自 ChangeNotifier,这样当共享的状态改变时,我们只需要调用 notifyListeners 来通知订阅者,然后订阅者重新构建 InheritedProvider,这也是第二个问题的答案,订阅类的实现如下:

代码语言:javascript复制
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
  final Widget child;
  final T data;

  ChangeNotifierProvider({Key key, this.child, this.data});

  @override
  _ChangeNotifierProviderState<T> createState() =>
      _ChangeNotifierProviderState<T>();

  ///定义一个便捷的方法,方便子树中的 widget 获取共享数据
  static T of<T>(BuildContext context) {
    final provider =
        context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>();
    return provider.data;
  }
}

class _ChangeNotifierProviderState<T extends ChangeNotifier>
    extends State<ChangeNotifierProvider<T>> {

  void update() {
    setState(() {});
  }

  @override
  void initState() {
    //给 model 添加监听器
    widget.data.addListener(update);
    super.initState();
  }

  @override
  void dispose() {
    //移除 model 的监听器
    super.dispose();
  }

  @override
  void didUpdateWidget(covariant ChangeNotifierProvider<T> oldWidget) {
    //当 Provider 更新时,如果旧数据不 ==,则解绑旧数据的监听,同时添加新数据的监听
    if (widget.data != oldWidget.data) {
      oldWidget.data.removeListener(update);
      widget.data.addListener(update);
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
    return InheritedProvider<T>(
      data: widget.data,
      child: widget.child,
    );
  }
}
复制代码

ChangeNotifierProvider 继承自 StatefulWidget,定义了一个 of 静态方法供子类方便获取 Widget 树的 InheritedProvider 中保存的共享状态

_ChangeNotifierProviderState 类的主要作用就是监听共享状态改变时重新构建 Widget 树。注意,在这个类中调用 setState() 方法,widget.child 始终是同一个,InheriedProvider 的 child 引用的始终是同一个子 Widget,所以 widget.child 并不会重新 build,这也就是相当于对 child 进行了缓存。当然如果 ChangeNotifierProvider 腹肌 Widget 重新 build 时,传入的 child 便有可能发生变化

现在我们需要的工具类都已经完成,下面通过根据一个例子看看如何使用上面的类

购物车示例
代码语言:javascript复制
///Item类,用于表示商品的信息
class Item {
  final price;
  int count;

  Item(this.price, this.count);
}

class CarMode extends ChangeNotifier {
  //用户保存购物车中商品列表
  final List<Item> _items = [];

  //禁止修改购物车里的商品信息
  UnmodifiableListView get items => UnmodifiableListView(_items);

  //购物车商品的总价
  double get totalPrice =>
      _items.fold(0, (value, item) => value   item.count * item.price);

  void add(Item item) {
    _items.add(item);
    //通知监听器(观察者),重新构建 InheritedProvider,更新状态
    notifyListeners();
  }
}

class ProviderTest extends StatefulWidget {
  @override
  _ProviderTestState createState() => _ProviderTestState();
}

class _ProviderTestState extends State<ProviderTest> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: ChangeNotifierProvider(
        data: CarMode(),
        child: Builder(
          builder: (context) {
            return Column(
              children: [
                Builder(builder: (context) {
                  var cart = ChangeNotifierProvider.of<CarMode>(context);
                  return Text("总价 :${cart.totalPrice}");
                }),
                Builder(
                  builder: (context) {
                    return RaisedButton(
                      child: Text("添加商品"),
                      onPressed: () {
                        ChangeNotifierProvider.of<CarMode>(context)
                            .add(Item(20, 1));
                      },
                    );
                  },
                )
              ],
            );
          },
        ),
      ),
    );
  }
}
复制代码

Item类:用于保存商品的信息系

CartMode 类:保存购物车内上面数据的类,即跨组件需要共享的 model 类

ProviderTest:最终构建的页面

每次点击添加商品,总价就会增加 20,虽然这个例子比较简单,只更新了同一个路由页中的一个状态,但是如果是一个真正的购物车,他的购物车数据通常会在 app 内共享,例如跨路由共享,将 ChangeNotifierProvider 放在整个应用的 Widget 树的根上,那么整个 app 就可以共享购物车的数据了

Provider 的原理图如下:

Model 变化后会自动通知 ChangeNotifierProvider (订阅者),ChangeNotifierProvider 内部会重新构建 InheritedWidget ,而依赖该 InheritedWidget 的子孙 Widget 就会更新

我们可以发现使用 Provider,将会带来如下好处:

1,我们的业务代码更加的关注数据,只需要更新 Model,则 UI 会自动更新,而不用在状态改变后在去手动调用 setState 来显式的更新页面

2,数据改变的消息传递被屏蔽了,我们无需手动去处理改变事件的发布和订阅了,这一切都被封装在 Provider 中了,这帮我们省掉了大量的工作

3,在大型复杂的应用中,尤其是需要全局共享的状态非常多的时候,使用 Provider 将会大大简化我们的代码逻辑,降低出错概率,提高开发效率

优化

上面实现的 ChangeNotifierProvider 是有两个明显的缺点:代码组织问题和性能问题,下面我们来看一下

代码组织问题
代码语言:javascript复制
Builder(builder: (context) {
  var cart = ChangeNotifierProvider.of<CarMode>(context);
  return Text("总价 :${cart.totalPrice}");
}),
复制代码

这段代码有两点可以优化

1,需要显示的调用 ChangenotifierProvider ,当 APP 内部依赖 CartMode 很多时,这样的代码就会很沉余

2,语义不明确,由于 ChangenotifierProvider 是订阅者,依赖 CarMode 的 Widget 自然就是订阅者,其实也就是状态的消费者;如果使用 Builder 来构建,语义就不是很明确,如果能使用一个更具有明确语义的 Widget,如 Consumer ,这样最终代码的语言就很明确,只要看到 Consumer,我们就知道他是某个跨组件或者全局的状态。

为了优化这个问题,我们可以封装一个 Consumer Widget,如下:

代码语言:javascript复制
class Consumer<T> extends StatelessWidget {
  
  final Widget child;
  final Widget Function(BuildContext context, T value) builder;

  Consumer({Key key, @required this.builder, this.child});

  @override
  Widget build(BuildContext context) {
    return builder(context, ChangeNotifierProvider.of<T>(context)); //自动获取 model
  }
  
}
复制代码

Cusumer 实现非常简单,它通过指定模板参数,然后内部自动调用 ChangeNotifierProvider.of 获取相应的 Mode,并且 Consumer 这个名字本身也是具有确切语义(消费者),现在上面的代码可以优化成下面这样:

代码语言:javascript复制
Consumer<CarMode>(
  builder: (context, cart) => Text("总价:${cart.totalPrice}"),
),
复制代码

是不是很优雅呢?

性能问题

上面代码还有一个性能问题,在 添加按钮的地方:

代码语言:javascript复制
Builder(
  builder: (context) {
    return RaisedButton(
      child: Text("添加商品"),
      onPressed: () {
        ChangeNotifierProvider.of<CarMode>(context)
            .add(Item(20, 1));
      },
    );
  },
)
复制代码

点击添加商品按钮后,由于购物车总价会发生变化,所以显示总结的 Text 是符合预期的。

但是 添加商品 按钮本身没有啥变化,所以他是不应该被重新 build 的,但是运行发现,每次点击的时候按钮都会被重新build。这是为什么呢,这是因为 RadisedButton 的 build 中调用了 ChangeNotifierProvider.of() ,也就是说依赖了 Widget树上面的 InheritedWidget (即 InheritedProvider) Widget ,

所以当添加完商品后,CartMode 发生了变化,会通知子树,以 InheritedProvider 将会更新,此时依赖他的子孙 Wdiget 都会被重新构建。

问题找到了,那么如何避免这个不必要的重构呢?

既然问题是 按钮和 InheritedWidget 建立了依赖联系,那么我们只要打破这种依赖关系就好了,如何打破呢,在最上面的时候就讲过了:调用 dpendOnInheritedWidgetOfExactTypegetElementForInheritedWidgetOfExactType 的区别就是前者会注册依赖关系,而后者则不会,所有只需要将 ChangeNotiferProvider.of 的实现改为下面这样即可:

代码语言:javascript复制
///listen:是否建立以来关系
static T of<T>(BuildContext context, {bool listen = true}) {
  final provider = listen
      ? context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>()
      : context
          .getElementForInheritedWidgetOfExactType<InheritedProvider<T>>()
          ?.widget as InheritedProvider<T>;
  return provider.data;
}
复制代码

修改后再次运行,就会发现按钮不会重新构建了,而总价任然后更新。

至此,我们实现了一个迷你版的 Provider,它具备 Pub 上 Provider package 的核心功能,但是由于我们的功能并不全面,只实现了一个可监听的 ChangeNotiferProvider,并没有实现数据共享,另外,我们的实现有些边界值没有考虑到,比如如何保证在 Widget 树重新 build 时 Mode 始终是单例等等。所以建议项目中还是使用 Provider Package,这篇文章只是帮助大家了解 Provider Package 的底层原理

本文参考 Fluuter实战 书籍

0 人点赞