美文网首页
如何自定义一个Provider

如何自定义一个Provider

作者: brock | 来源:发表于2022-11-21 15:18 被阅读0次

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,性能问题也就被解决了

相关文章

网友评论

      本文标题:如何自定义一个Provider

      本文链接:https://www.haomeiwen.com/subject/skknxdtx.html