Fiutter- 案例4 (选择话题Provider)

前言

我们通过一个实际页面来使用并且理解一下Flutter Provider的使用,了解下Provider如果进行状态存储以及共享

页面

微信图片_20220604131605.jpg
微信图片_20220604131740.jpg

InheritedWidget

InheritedWidget是一个功能性的组件,可以在组件树种从上往下的进行数据共享,定义在InheritedWidget组件中的数据可以被其子节点获取到,并且当InheritedWidget刷新时,所以依赖它的子节点都会进行刷新

创建一个需要共享的数据类

class ShareData{

  bool isEnableBiometric;

  void setIsEnableBiometric(bool isEnableBiometric){
    this.isEnableBiometric = isEnableBiometric;
  }

  ShareData(this.isEnableBiometric);
}

创建一个InheritedWidget
里面包含需要被共享的数据

class ShareWidget extends InheritedWidget {
  ShareData shareData;

  static ShareWidget? of(BuildContext context) {
    //会将调用该方法的Widget进行注册,当数据刷新时咋会对其进行刷新,所注册组件的会调用didChangeDependencies -> build
    return context.dependOnInheritedWidgetOfExactType();
  }

  static ShareData? ofValue(BuildContext context) {
    //会将调用该方法的Widget不会进行注册
    return context.findAncestorWidgetOfExactType()?.shareData;
  }

  ShareWidget({required this.shareData, Key? key, required Widget child})
      : super(key: key, child: child);

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    ///框架是否通知继承于这个组件并注册的子组件
    return true;
  }
}

static ShareWidget? of(BuildContext context)
由于子节点需要使用共享的数据,所以需要暴露出函数让子节点可以拿到父或者祖节点的InheritedWidget从而拿到共享数据,并且在调用findAncestorWidgetOfExactType时子Widget会进行注册,从而在日后InheritedWidget变化时可以被刷新,当组件树中的InheritedWidget更新时会通知所有已经注册的子组件进行刷新状态

updateShouldNotify
表示已经注册的子Widget是否会在InheritedWidget变化时刷新,当子Widget被通知刷新时会调用WidgetdidChangeDependencies函数

在主页面定义初始化共享数据

在主页面使用InheritedWidget

class TestSharePageState extends State {

  ShareData shareData = ShareData(false);

  @override
  void initState() {
    super.initState();
    Future.delayed(Duration(seconds: 5),(){
      setState(() {
        shareData.setIsEnableBiometric(true);
      });
    });
  }
  @override
  Widget build(BuildContext context) {
    return ShareWidget(shareData: shareData,
        child: Column(
          children: [
            TestSharePage1(),
            TestSharePage2(),
            TestSharePage3(),
          ],
        ));
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('TestSharePageState didChangeDependencies');
  }
}

在子页面使用共享数据

class TestSharePage1State extends State {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text(
          'TestSharePage1${ShareWidget.of(context)?.shareData.isEnableBiometric ?? null}'),
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('TestSharePage1State didChangeDependencies');
  }
}
class TestSharePage2State extends State {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text(
          'TestSharePage2${ShareWidget.of(context)?.shareData.isEnableBiometric ?? null}'),
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('TestSharePage2State didChangeDependencies');
  }
}
class TestSharePage3State extends State {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text('TestSharePage3 test'),
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('TestSharePage3State didChangeDependencies');
  }
}

刷新共享数据
主界面在5秒之后刷新了共享数据

  @override
  void initState() {
    super.initState();
    Future.delayed(Duration(seconds: 5),(){
      setState(() {
        shareData.setIsEnableBiometric(true);
      });
    });
  }

五秒之后会打印,并且Page1Page2都会刷新,Page3由于没有注册所以不会刷新

I/flutter ( 3479): TestSharePage1State didChangeDependencies
I/flutter ( 3479): TestSharePage2State didChangeDependencies

只使用数据而不注册
上述测试中,Page3由于没有使用共享数据,所以共享数据在刷新的时候Page3没有注册所以没有刷新,我们也可以只使用Page3但是不注册这样就不会刷新了

使用下列方式可以只是使用而不进行注册

  static ShareData? ofValue(BuildContext context) {
    //会将调用该方法的Widget不会进行注册
    return (context.getElementForInheritedWidgetOfExactType()?.widget as ShareWidget).shareData;
  }
class TestSharePage3State extends State {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Text('TestSharePage3 ${ShareWidget.ofValue(context)?.isEnableBiometric ?? 'null'}}'),
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('TestSharePage3State didChangeDependencies');
  }
}

此时进行数据刷新,则Page3不会调用didChangeDependencies,但是数据依旧刷新了,因为父节点在重新构建时,子节点也会刷新

Provider

上面我们基本了解了InheritedWidget是因为父子节点的关系所以可以子啊父节点中存储数据,子节点中使用数据从而进行数据共享以及局部页面刷新等操作,Provider框架也是基于这样的原理去构建的一个更好用的全局状态管理,数据共享,局部刷新框架

我们来使用Provider框架去实现我们最开偷那两张图片的案例

首先我们添加provider依赖

provider: ^4.0.4

简单分析一下案例

案例中我们需要选择多个兴趣标签,并且会显示你已经选择了几个标签,然后进行提交,那么我们所共享的数据就是用户所选择的兴趣标签,并且共享数据的范围就是当前页面

共享数据的提供: 只是在当前页面的顶部对共享数据进行提供以及初始化,默认选中的兴趣标签数组数量为0
需要使用到共享数据的组件:
1.每个兴趣的item需要使用到共享数据用以判断当前item是否需要被选中(变色)
2.已选择数量所展示的Text
3.底部的提交按钮需要用到共享数据,但是只是使用而已

Provider的介绍

官方文档

它是对InheritedWidget的一个分封装以及扩展,提供了更好的性能以及更简单的使用方式等

创建需要共享的数据类

class Topics {
  var _chooseTopics = [];

  List get chooseTopics => _chooseTopics;

  void chooseTopic(String topic) {
    if (!_chooseTopics.contains(topic)) {
      _chooseTopics.add(topic);
    } else {
      _chooseTopics.remove(topic);
    }
  }
}

将共享数据通过Provider暴露

class ChooseTopicPage extends StatefulWidget {
  @override
  State createState() {
    return ChooseTopicState();
  }
}

class ChooseTopicState extends State {
  Topics _topics = Topics();

  @override
  Widget build(BuildContext context) {
    print('ChooseTopicState build');
    return Scaffold(
        appBar: AppBar(
            centerTitle: true,
            title: Text('Topics', style: TextStyle(color: Colors.black)),
            backgroundColor: Colors.white,
            leading: Icon(Icons.arrow_back_ios, color: Colors.green)),
        body: Provider(
          create: (_) => _topics,
          child: Container(
            padding: EdgeInsets.all(20),
            child: Column(children: [
              TopicWrap(),
              Container(
                height: 40,
                decoration: BoxDecoration(
                    borderRadius: BorderRadius.all((Radius.circular(5))),
                    color: Colors.green),
                child: MaterialButton(
                  textColor: Colors.white,
                  onPressed: () => {
                  },
                  child: Text('选择'),
                ),
              ),
            ]),
          ),
        ));
  }
}

这里的TopicWrap是一个封装的Widget,在里面会使用到共享数据

class TopicWrapState extends State {

  List topics = [
    "欧美电影",
    "日本电影",
    "日本动漫",
    "大陆电影",
    "恐怖电影",
    "豆瓣Top250",
  ];

  List getWrapList() {
    var childrens = [];
    for (var i = 0; i < topics.length; i++) {
      childrens.add(GestureDetector(
          child: Container(
            padding: EdgeInsets.all(5),
            margin: EdgeInsets.all(10),
            decoration: BoxDecoration(
                borderRadius: BorderRadius.all(Radius.circular(5)),
                color: Provider
                    .of(context).chooseTopics.contains(topics[i])
                    ? Colors.orange
                    : Colors.green),
            child: Text(
              topics[i],
              style: TextStyle(color: Colors.white),
            ),
          ),
          onTap: () {
            Provider.of(context,listen: false).chooseTopic(topics[i]);
            setState(() {

            });
          }));

    }
    return childrens;
  }

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      Wrap(children: getWrapList()),
      Container(
        margin: EdgeInsets.all(20),
        child: Text('已选择${Provider
            .of(context)
            .chooseTopics
            .length}'),
      ),
    ],);
  }

}

这里使用Provider.of获取到了共享数据的长度
Provider.of(context).chooseTopics.length

并且在点击条目的时候也是使用同样的方式获取共享数据然后修改数据,这是使用listen: false是因为这里并没有组件使用并显示数据,所以并不需要对其进行注册
Provider.of(context,listen: false).chooseTopic(topics[i])

然后在修改数据之后使用了setState(() { })进行页面刷新,然后就可以达到更新的效果

所以我们的结构是:

图片1.png

这样可以实现功能,Provider只是提供了数据存储的功能,并且每次数据变化都个要刷新整个Wrap页面从而达到对应的效果

ChangeNotifyProvider

使用ChangeNotifyProvider,它会在数据更新之后通知所有注册的Widget去进行build,不用我们手动的去更新页面

修改数据类

ChangeProvider需要一个ChangeNotifier类型的共享数据,并且需要在数据被操作需要刷新时调用notifyListeners()函数

class Topics extends ChangeNotifier {
  var _chooseTopics = [];

  List get chooseTopics => _chooseTopics;

  void chooseTopic(String topic) {
    if (!_chooseTopics.contains(topic)) {
      _chooseTopics.add(topic);
    } else {
      _chooseTopics.remove(topic);
    }
    notifyListeners();
  }
}

Provider修改为ChangeNotifyProvider

然后移除掉上述案例中的setState(() { }),还是可以达到刚才的效果

Consumer

在我们的案例中底部还有一个选择的确认按钮,他需要拿到共享数据然后做一些其他的业务操作,我们给他加上获取数据的代码

  @override
  Widget build(BuildContext context) {
    print('ChooseTopicState build');
    return Scaffold(
        appBar: AppBar(
            centerTitle: true,
            title: Text('Topics', style: TextStyle(color: Colors.black)),
            backgroundColor: Colors.white,
            leading: Icon(Icons.arrow_back_ios, color: Colors.green)),
        body: ChangeNotifierProvider(
          create: (_) => _topics,
          child: Container(
            padding: EdgeInsets.all(20),
            child: Column(children: [
              TopicWrap(),
              Container(
                height: 40,
                decoration: BoxDecoration(
                    borderRadius: BorderRadius.all((Radius.circular(5))),
                    color: Colors.green),
                child: MaterialButton(
                  textColor: Colors.white,
                  onPressed: () => {
                    ///新增代码
                    print('选择完毕-${ Provider.of(context).chooseTopics}')
                  },
                  child: Text('选择'),
                ),
              ),
            ]),
          ),
        ));
  }

当我们点击按钮时会报错

Error: Could not find the correct Provider above this ChooseTopicPage Widget

This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:

为什么同为ChildTopicWrap不会有这个异常?

这是因为
Provider是根据InheritedWidget去实现的,是根据父子关系视图树进行共享书嫉妒而实现
TopicWrap 是由一个新的BuildContext去创建的,他已经拥有了所有Parent节点包括Provider,所以它可以获取到Provider以及它里面的共享数据,而MaterialButton是和Provider在一个BuildContext下,他们是在同一个BuildContext下创建以及初始化,MaterialButton并非Provider的后代控件

解决: 我们可以使用Providerbuild属性他返回一个新的Context供我们使用,基于新的context我们是可以拿到Provider

body: ChangeNotifierProvider(
          create: (_) => _topics,
          builder: (context,child){
            return Container(
              padding: EdgeInsets.all(20),
              child: Column(children: [
                TopicWrap(),
                Container(
                  height: 40,
                  decoration: BoxDecoration(
                      borderRadius: BorderRadius.all((Radius.circular(5))),
                      color: Colors.green),
                  child: MaterialButton(
                    textColor: Colors.white,
                    onPressed: () => {
                    print('选择完毕-${ Provider.of(context,listen: false).chooseTopics}')
                  },
                    child: Text('选择'),
                  ),
                ),
              ]),
            );
          },
        )

我们也可以使用Consumer去优化我们的Provider处理,让我们只是刷新数据变化的部分不用调用build函数,而且也不需要在顶层去声明Provider嵌套复杂

  • 它不需要在在顶层预先申明一个Provider
  • 并且只是更新数据变化的局部,而不是重新调用整个buld函数

使用Consumer包裹你使用到共享数据的控件

使用builder属性可以拿到新的context以及共享数据,然后当共享数据发生变化时,你也会进行此控件的局部刷新并不会调用当前的build做到局部刷新,也不用考虑控件的层级

ChangeNotifierProvider(
          create: (_) => _topics,
          child: Container(
            padding: EdgeInsets.all(20),
            child: Column(children: [
              TopicWrap(),
              Consumer(builder: (context, topics, child) {
                return Text('已选择${topics.chooseTopics}');
              }),
              Container(
                height: 40,
                decoration: BoxDecoration(
                    borderRadius: BorderRadius.all((Radius.circular(5))),
                    color: Colors.green),
                child: MaterialButton(
                  textColor: Colors.white,
                  onPressed: () => {
                    print(
                        '选择完毕-${Provider.of(context, listen: false).chooseTopics}')
                  },
                  child: Text('选择'),
                ),
              ),
            ]),
          ),
        )
微信图片_20220605161659.jpg

欢迎关注Mike的

Android 知识整理

你可能感兴趣的:(Fiutter- 案例4 (选择话题Provider))