前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter | 数据共享

Flutter | 数据共享

作者头像
345
发布2022-02-11 14:31:09
1.3K0
发布2022-02-11 14:31:09
举报
文章被收录于专栏:用户4077185的专栏

本文示例代码

数据共享 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实战 书籍

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/03/25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 数据共享 InheritedWidget
    • didChangeDependencies
      • 应该在 did.... 方法中做什么
        • 深入理解 InheritedWidget 方法
        • 跨组件状态共享 Provider
          • Provider
            • 购物车示例
              • 优化
                • 代码组织问题
                • 性能问题
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档