如何自定义一个Provider

1.简介

InheritedWidget是Flutter提供的一个非常重要的功能性组件,可以实现在widget树中从上到下数据共享数据,比如我们的widget树中有一个InheritedWidget,并且共享了一个数据,那么我们在InheritedWidge的任意子widget就可以获取到轻松InheritedWidget的共享数据,比如Theme和Local正是通过InheritedWidget实现在widget中共享数据的,再比如大名鼎鼎的Provider其实正是通过InheritedWidget实现数据共享和状态管理的

2.例子

我们现在通过一个计数器的例子演示一下InheritedWidget是如何实现数据共享的

首先写一个类继承自InheritedWidget,代码如下:

class ShareWidget extends InheritedWidget {

  final int count;

  const ShareWidget({super.key, required super.child, required this.count});

  /// 定义一个方法,方便子widget获取共享数据
  static ShareWidget? of(BuildContext context) {
     return context.dependOnInheritedWidgetOfExactType<ShareWidget>();
  }

  @override
  bool updateShouldNotify(covariant ShareWidget oldWidget) {
    /// 表示当数据发生改变的时候通知依赖InheritedWidget数据的子widget重新build
    return oldWidget.count != count;·
  }

}

再定义一个CountWidget,并且调用ShareWidget? of(BuildContext context)方法获取共享数据,代码如下:

class CountWidget extends StatefulWidget {
  CountWidget({super.key});

  @override
  State<StatefulWidget> createState() => _CountState();
}

class _CountState extends State<CountWidget> {
  @override
  Widget build(BuildContext context) {
    print('CountWidget build');
    final count = ShareWidget.of(context)?.count ?? 0;
    return Text(count.toString());
  }
}

然后在页面中使用CountWidget,代码如下:

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ShareWidget(count: _counter, child: const CountWidget()),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

运行工程,看到如下的界面

image

点击右下角增加按钮,发现中间的数据在增加,并且控制台打印CountWidget build,说明CountWidget获取共享数据成功了,并且每次获取数据的时候都会重新build一次,我们再修改一下CountWidget 的代码,将获取共享数据的代码去掉,代码如下:

class CountWidget extends StatefulWidget {
  const CountWidget({super.key});

  @override
  State<StatefulWidget> createState() => _CountState();
}

class _CountState extends State<CountWidget> {
  @override
  Widget build(BuildContext context) {
    print('CountWidget build');
    /// final count = ShareWidget.of(context)?.count ?? 0;
return Text('0');
  }
}

重新运行一下工程,发现控制台不会再打印CountWidget build,说明每次点击增加按钮的时候CountWidget 不会重新build,这就说明了当子widget依赖父InheritedWidget中的数据的时候,只要数据发生改变,子widget就会重新build,如果没有依赖共享数据,即使数据变化了,子widget也不会重新build

3.深入理解InheritedWidget

我们给_CountState 重写didChangeDependencies方法,代码如下:

class CountWidget extends StatefulWidget {
  const CountWidget({super.key});

  @override
  State<StatefulWidget> createState() => _CountState();
}

class _CountState extends State<CountWidget> {
  @override
  Widget build(BuildContext context) {
    print('CountWidget build');
    final count = ShareWidget.of(context)?.count ?? 0;
    return Text(count.toString());
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    /// build方法如果中没有依赖InheritedWidget的共享数据,此回调不会被调用。
print("didChangeDependencies");
  }
}

我们会发现每次InheritedWidget的共享数据发生变化的时候,控制台都会打印didChangeDependencies,说明didChangeDependencies()被调用了,但是如果我们把CountWidget对于共享数据的依赖去掉,每次共享数据发生改变的时候控制台都不会打印didChangeDependencies,说明didChangeDependencies()没有被调用,也就是说只要子widget不依赖InheritedWidget的共享数据,即便是整个页面被setState了,CountWidget 的didChangeDependencies()也不会被调用,我们来看看didChangeDependencies的源码注释,如下:

 /// Called when a dependency of this [State] object changes.
///
/// For example, if the previous call to [build] referenced an
/// [InheritedWidget] that later changed, the framework would call this
/// method to notify this object about the change.
///
/// This method is also called immediately after [initState]. It is safe to
/// call [BuildContext.dependOnInheritedWidgetOfExactType] from this method.
///
/// Subclasses rarely override this method because the framework always
/// calls [build] after a dependency changes. Some subclasses do override
/// this method because they need to do some expensive work (e.g., network
/// fetches) when their dependencies change, and that work would be too
/// expensive to do for every build.
@protected
@mustCallSuper
void didChangeDependencies() { }

这段注释最核心的注释其实就是第一句,意思就是说当这个State的对象的依赖发生改变的时候调用,这里所说的依赖其实指的是子widget对父InheritedWidget的共享数据的依赖,当共享数据发生变化的时候,这个didChangeDependencies方法就会被调用,如果我们只是想在CountWidget 使用共享数据,不希望共享数据发生变化的时候didChangeDependencies方法被调用,该如何做呢?其实很简单,只要修改ShareWidget.of(context)方法实现就可以了 修改代码如下就可以了

static ShareWidget? of(BuildContext context) {
  // return context.dependOnInheritedWidgetOfExactType<ShareWidget>();
  return context
      .getElementForInheritedWidgetOfExactType<ShareWidget>()
      ?.widget as ShareWidget;
}

这样修改后当依赖的共享数据发生变化的时候,CountWidget 的build和 didChangeDependencies方法都不会被调用,为什么会这样呢?我们来看一下dependOnInheritedWidgetOfExactType()和getElementForInheritedWidgetOfExactType()方法的内部实现有什么区别,代码如下:

@override
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  return ancestor;
}

@override
InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  //多出的部分
  if (ancestor != null) {
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}

可以看到dependOnInheritedWidgetOfExactType()比getElementForInheritedWidgetOfExactType()多调了一个dependOnInheritedElement()方法,我们再看看dependOnInheritedElement()的实现,代码如下

@override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

我们其实可以看到InheritedWidget 和依赖它的子组件注册了依赖关系,这也就是为什么使用dependOnInheritedWidgetOfExactType()会导致子组件的build()和didChangeDependencies()在共享数据发生变化后被重新调一次的原因,了解了这一点,对我们后面开发一个简易的Provider会有帮助的,后面会说明为什么会有帮助,这里先卖个关子

4.自定义Provider

1. 自定义过程

我们将基于上面对InheritedWidget的理解来实现一个最简单的Provider,因为我们需要在组件树中共享数据,所以对于需要监听共享数据的子孙widget, 我们需要使用一个InheritedWidget 将它包起来,然后让这个子孙widget监听InheritedWidget 提供的共享数据,同时,为了代码的复用性和通用性,我们需要写一个类继承自InheritedWidget ,并且结合泛型,实现共享数据的抽象,代码如下:

class ProviderDataWidget<T> extends InheritedWidget {
  const ProviderDataWidget({super.key,
    required this.data,
    required Widget child,
  }) : super(child: child);

  final T data;

  @override
  bool updateShouldNotify(ProviderDataWidget<T> old) {
    //先简单返回true,这样一来不够数据变化前是否一致,都会调用依赖其的子孙节点的`didChangeDependencies`。
    return true;
  }
}

凡是需要监听共享数据的widget,我们都让它成为ProviderDataWidget的子孙widget

我们在实际开发中,widget和业务逻辑一般都是分开写在不同的类里的,一般业务逻辑我们都是写在ViewModel或者Provider类中,那么,在这些写业务逻辑的类中,我们如何在数据发生改变的时候通知widget 刷新页面呢?我们看了Provider状态管理框架的源码后发现我们可以使用ChangeNotifier,

ChangeNotifier的源码大致如下:

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());
  }
   
  ... //省略无关代码
}

ChangeNotifier 实现了一种发布者-订阅者模式,我们同通过addListener方法添加监听方法,removeListener方法移除监听方法,notifyListeners方法回调所有监听方法,现在我们只需要将我们的业务逻辑放在一个Provider类中,让它继承自ChangeNotifier,成为一个发布者,每当数据有变化的时候通知widget刷新

有了发布者,我们还需要一个订阅者,订阅者其实就是widget,但是,为了代码的复用性和通用性,我们需要封装一个通用的widget,以此来完成自动订阅,自动解绑订阅,提供共享数据,代码如下:

class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
  const ChangeNotifierProvider({super.key,
    required this.data,
    required this.child,
  });

  final Widget child;
  final T data;

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

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


class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>> {
  void update() {
    //如果数据发生变化(provider类调用了notifyListeners),重新构建InheritedProvider
    setState(() => {});
  }

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

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

  @override
  void dispose() {
    // 移除provider的监听器
    widget.data.removeListener(update);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    print('_ChangeNotifierProviderState build');
    return ProviderDataWidget<T>(
      data: widget.data,
      child: widget.child,
    );
  }

我们在_ChangeNotifierProviderState实现了对provider的监听,当监听的provider刷新监听方法的时候,这个类的setState()方法就会被调用,从而完成widget的刷新,但是呢?因为刷新的时候,widget.child始终是同一个,所以widget.child如果没有监听共享数据,widget.child是不会被刷新的

2. 使用

我们将展示用户信息,并且在用户信息改变的时候刷新widget,展示最新的用户信息

我们需要在provider中提供用户信息,并且在用户信息改变的时候刷新widget,代码如下:

class UserInfoProvider extends ChangeNotifier {
  var _userInfo = UserInfo('xiaoHong', 11);

  get userInfo => _userInfo;

  set userInfo(value) {
    _userInfo = value;
    notifyListeners();
  }

  UserInfo getUserInfo() => _userInfo;
}

我们还需要在widget中使用ChangeNotifierProvider,监听用户数据的变化,代码如下:

class UserInfoWidget extends StatefulWidget {
  const UserInfoWidget({super.key});

  @override
  State<StatefulWidget> createState() => UserInfoState();
}

class UserInfoState extends State<UserInfoWidget> {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<UserInfoProvider>(
        data: UserInfoProvider(),
        child: Column(
          children: [
            Builder(builder: (context) {
              print('user text build');
              final userInfo =
                  ChangeNotifierProvider.of<UserInfoProvider>(context).userInfo;
              return Text('${userInfo.name}, ${userInfo.age}岁');
            }),
            const SizedBox(
              height: 30,
            ),
            Builder(builder: (context) {
              print('change text build');
              return GestureDetector(
                onTap: () {
                  ChangeNotifierProvider.of<UserInfoProvider>(context)
                      .userInfo = UserInfo('xiaoNan', 17);
                },
                child: const Text('change'),
              );
            })
          ],
        ));
  }
}

运行代码后出现界面如下

image

我们点击一下 change Text,修改用户信息,发现界面会被刷新展示正确的新的用户信息,说明我们初步的provider已经完成,可以让widget监听数据的变化,实现状态共享,而且可以使widget和业务逻辑相分离

3. 改进

上面的做法其实是存在一些性能问题的,每次点击 change widget的时候控制台都会打印‘change text build’,这说明change Text被rebuild了,这是不对的,因为change Text并没有监听共享数据,不应该在数据变化的时候刷新,那么这个问题该如何解决了,其实很简单,还记得在上面第三点深入理解InheritedWidget卖了个关子吗?我们只需要修改ChangeNotifierProvider.of方法的实现即可,代码如下:

//添加一个listen参数,表示是否建立依赖关系
 static T of<T>(BuildContext context, {bool listen = true}) {
  final provider = listen
      ? context.dependOnInheritedWidgetOfExactType<ProviderDataWidget<T>>()
      : context.getElementForInheritedWidgetOfExactType<ProviderDataWidget<T>>()?.widget
  as ProviderDataWidget<T>;
  return provider!.data;
}

我们添加了一个listen参数,表示是否需要让子孙widget和InheritedWidget建立依赖关系,如果listen为false,表示不需要建立依赖,那么在共享数据发生变化的时候就不会刷新子孙widget

我们再修改一下change widget的代码,如下所示:

Builder(builder: (context) {
  print('change text build');
  return GestureDetector(
    onTap: () {
      ChangeNotifierProvider.of<UserInfoProvider>(context,listen: false)
          .userInfo = UserInfo('xiaoNan', 17);
    },
    child: const Text('change'),
  );
})

然后我们再点击change widget,发现控制台不会再change text build,说明change widget没有被rebuild,性能问题也就被解决了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容