浅析Flutter中的状态管理和Provider的使用

前言

Flutter作为谷歌的移动UI框架,近今年的热度还是很高的。本人在实际项目中,发现flutter的体验还是不错的。当然体验良好的前提是对flutter的状态管理方式有比较深入的理解。而要理解flutter的状态管理方式,通过对flutter官方推荐的状态管理工具——Provider的学习,是一种相当不错的方法。这也是我写作这篇文章的原因。一来可以总结自己学到的知识,加深印象。二来是希望帮助更多的人用好Provider。由于本人水平有限,文章难免有错漏之处,希望大家多多批评指正。

1.1从flutter的状态管理方式谈起

响应式的编程语言基本都会涉及到状态管理的概念。什么是状态呢?对于用户能看到的界面而言,我们能看到外观,比如按钮的颜色这些,可以称之为一种状态。而对于用户的数据,比如音乐播放列表里面的歌曲名称,也可以称之为一种状态。而对于用户看不到的部分,比如当前手机的网络、蓝牙、GPS的状态,也可以称之为一种状态。当然,对于那些看不见的状态,我们也可以通过一些方式,以界面的方式可视化,在此先不纠结这些细节。

对于这些状态该如何进行管理呢?根据《Flutter实战》中推荐的方法,对于有父子或者亲戚关系的Widget的状态,我们有以下3种管理方式:

  • Widget 管理自己的状态。

  • Widget 管理子 Widget 状态。

  • 混合管理(父 Widget 和子 Widget 都管理状态)。

感兴趣的朋友可以去看看这本书的内容。此处不再赘述。

以上的方法,只针对有父子或者亲戚关系的Widget的状态管理。但是对于没有什么亲戚关系,比如跨路由的状态该如何管理呢?有两种方式。

一种是借鉴父、子Widget之间的状态管理方式。把Widget中需要共享的状态提高到一个足够高的位置,然后在状态发生改变时通过订阅者模式通知界面进行更新。

另一种是使用flutter中的InheritedWidget,Provider插件本质上就是对InheritedWidget的封装,所有这是比较推荐的一种方式。我们后面会详细介绍这种方式。而对于第一种方式,虽然我们不推荐使用,但是对这种方式的学习可以帮助我们理解flutter中InheritedWidget设计的精妙之处。所以我会举一个详细的例子帮助大家理解这种状态管理方式。下面请看例子:

1.2无InheritedWidget下的状态管理实例

现在假设我们有这样一个需求:写一个进度条控件,每次拖动进度条的时候返回一个进度值,指示当前进度。为了方便,我们用一个整数值表示进度条进度,界面部分省去,很容易就写出下面这样的代码(这其实就是一个计数器):

class ProgressBar extends StatefulWidget {
   ProgressBar({Key? key,required this.onProgressChange}) : super(key: key);
   final void Function(int value)? onProgressChange;

  @override
  _ProgressBarState createState() => _ProgressBarState();
}

class _ProgressBarState extends State<ProgressBar> {
  int progressValue = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          color: Colors.blue[100],
          child: Text("value:$progressValue"),
        ),
        ElevatedButton(onPressed: () {
          progressValue++;
          setState(() {
            widget.onProgressChange!.call(progressValue);
          });
        }, child: Text("increase")),
      ],
    );
  }
}

我们这样使用ProgressBar:

@override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
          child: ProgressBar(onProgressChange: (value){
           print("current progress value:$value");
          },),
        ));
  }

上面就是子widget管理自己的状态,然后点击按钮的时候通过回调将进度条的值返回给父Widget的例子。

目前来看上面的代码没有任何问题,父Widget可以完美地获取到子Widget的状态。 但是如果现在又有一个需求,要在进度条之外控制进度条的进度(比如:滑动屏幕而不是拖动进度条)上面的写法就不适用了。因为父Widget没办法控制子Widget的状态。

该怎么解决这个问题呢?显然这里需要共享的状态是progressValue的值,我们要把这个状态提升一下,比如提到父Widget,这样就可以通过父Widget控制进度条的进度了。我们给每个ProgressBar新增一个ProgressBarController成员,而父Widget持有ProgressBarController,将ProgressBarControlle传给子Widget。我们期望通过ProgressBarController可以控制子Widget状态的更新。 代码如下:

class ProgressBarController {
  int progressValue = 0;
}

class ProgressBar extends StatefulWidget {
  ProgressBar(
      {Key? key,
      required this.onProgressChange,
      required this.progressBarController})
      : super(key: key);
  final void Function(int value)? onProgressChange;
  final ProgressBarController progressBarController;

  @override
  _ProgressBarState createState() => _ProgressBarState();
}

class _ProgressBarState extends State<ProgressBar> {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          color: Colors.blue[100],
          child: Text("value:${widget.progressBarController.progressValue}"),
        ),
        ElevatedButton(
            onPressed: () {
              setState(() {
                widget.progressBarController.progressValue++;
              });
            },
            child: Text("increase")),
      ],
    );
  }
}

使用上面的进度条组件:

  late ProgressBarController progressBarController;

  @override
  void initState() {
    super.initState();
    progressBarController = ProgressBarController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Column(
          children: [
            Center(
              child: ProgressBar(
                progressBarController: progressBarController,
                onProgressChange: (value) {
                  print("current progress value:$value");
                },
              ),
            ),
            ElevatedButton(
              onPressed: () {
                progressBarController.progressValue++;
              },
              child: Text("increase child"),
            ),
          ],
        ));
  }

运行结果如下:

 可以看到有两个按钮,increase是进度条自身管理的Widget,increas child是父Widget中的按钮,我们期望点击按钮。value的值都会更新。然而事与愿违,点击下面的按钮,界面上value的值没有更新。问题出在哪里呢?仔细看代码会发现,父Widget在点击按钮时没有调用setState(),调用setState()即可。代码如下:

// before			
ElevatedButton(
    onPressed: () {
        progressBarController.progressValue++;
    },
    child: Text("increase child"),
),

// after
ElevatedButton(
    onPressed: () {
        setState(() {
            progressBarController.progressValue++;
        });
    },
    child: Text("increase child"),
 ),	

需求是解决了,但是这样写存在什么问题呢?

一个很严重的问题在于,我们要更新的是子Widget的状态,却在父Widget中调用的setState()函数。这样我们更新的是父Widget,父Widget再更新子Widget。显然父widget不应该被作没必要的更新。

有什么办法能只更新子Widget的状态而父Widget不改变呢?答案是订阅者模式。只需要在父Widget的按钮被点击时,通知子Widget更新即可。借用flutter中的changeNotifier可以轻松得实现这种模式,代码如下:

class ProgressBarController extends ChangeNotifier{
  int progressValue = 0;

  increaseProgressValue(){
    progressValue++;
    notifyListeners(); // 通知界面更新
  }
}

在ProgressBar中使用ProgressBarController添加订阅事件:

class ProgressBar extends StatefulWidget {
  ProgressBar(
      {Key? key,
      required this.onProgressChange,
      required this.progressBarController})
      : super(key: key);
  final void Function(int value)? onProgressChange;
  final ProgressBarController progressBarController;

  @override
  _ProgressBarState createState() => _ProgressBarState();
}

class _ProgressBarState extends State<ProgressBar> {
  @override
  void initState() {
    super.initState();
    widget.progressBarController.addListener(_update);
  }

  @override
  void dispose(){
    widget.progressBarController.removeListener(_update);
    super.dispose();
  }

  _update(){
    setState(() {
    });
  }

  @override
  Widget build(BuildContext context) {
    print("build.");
    return Column(
      children: [
        Container(
          color: Colors.blue[100],
          child: Text("value:${widget.progressBarController.progressValue}"),
        ),
        ElevatedButton(
            onPressed: () {
              widget.progressBarController.increaseProgressValue();
            },
            child: Text("increase")),
      ],
    );
  }
}

使用:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Column(
          children: [
            Center(
              child: ProgressBar(
                progressBarController: progressBarController,
                onProgressChange: (value) {
                  print("current progress value:$value");
                },
              ),
            ),
            ElevatedButton(
              onPressed: () {
                progressBarController.increaseProgressValue();
              },
              child: Text("increase child"),
            ),
          ],
        ));
  }

现在再点击increa child按钮,就无需在父Widget中调用setState()了,父Widget也就无需更新。如果将ProgressBarController定义成全局变量,就可以实现跨路由的状态管理了。

上面的代码虽然能够实现全局的状态管理,但是每次订阅事件,然后移除事件,在项目复杂时未免过于繁琐。所以并不适用于业务逻辑的控制,而是更适合自定义小组件的状态管理。比如例子中的进度条控件(虽然没有界面,后续可以完善)。

理想的方法是使用InheritedWidget,或者借用封装了InheritedWidget组件的Provider插件。

1.3InheritedWidget

InheritedWidget是什么?根据《flutter实战》中的描述:

InheritedWidget是 Flutter 中非常重要的一个功能型组件,它提供了一种在 widget 树中从上到下共享数据的方式,比如我们在应用的根 widget 中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget 中来获取该共享的数据!这个特性在一些需要在整个 widget 树中共享数据的场景中非常方便!如Flutter SDK中正是通过 InheritedWidget 来共享应用主题(Theme)和 Locale (当前语言环境)信息的。

由于使用Provider不需要直接操作InheritedWidget,所以在此不作过多的介绍。我们直接看看如何使用Provider实现一个和上面一样的进度条的状态管理。

1.4使用Provider的状态管理

下面我们看看怎么使用Provider实现同样效果的进度条。

首先,ProgressBarController的代码可以保持不变:

class ProgressBarController extends ChangeNotifier {
  int progressValue = 0;

  increaseProgressValue() {
    progressValue++;
    notifyListeners(); // 通知界面更新
  }
}

然后,在程序启动时,直接启动MultiProvider,创建需要共享的”状态“,这里也就是ProgressBarController。MultiProvider顾名思义就是可以创建多个不同的”状态“。当然,这些状态不能是同一类的。代码如下:

void main() {
  runApp(MultiProvider(
    providers: [
      ChangeNotifierProvider(create: (_) => ProgressBarController()),
    ],
    child: MyApp(),
  ));
}

使用Provider后,ProgressBar的实现就变得简洁了。调用context.watch<ProgressBarController>()方法。该方法会寻找离ProgrsesBar最近的ProgressBarController祖先。找到后边可以读取ProgressBarController中的内容。这里是progressValue。代码如下:

class ProgressBar extends StatefulWidget {
  ProgressBar({Key? key}) : super(key: key);

  @override
  _ProgressBarState createState() => _ProgressBarState();
}

class _ProgressBarState extends State<ProgressBar> {
  @override
  Widget build(BuildContext context) {
    return Container(
        child: Text("value:" + context.watch<ProgressBarController>().progressValue.toString()));
  }
}

使用Provider后怎么使用ProgressBar呢?代码如下,调用 context.read<ProgressBarController>()同样可以获取ProgressBarController实例,然后调用increaseProgressValue()方法。因为increaseProgressValue()方法中调用了notifyListeners(),所以value的值会更新。

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text("learn provider")),
        body: Column(
          children: [
            Center(
              child: Container(
                child: ProgressBar(),
              ),
            ),
            ElevatedButton(
                onPressed: () {
                  context.read<ProgressBarController>().increaseProgressValue();
                },
                child: Text("increase")),
          ],
        ),
      ),
    );
  }
}

到这里读者们可能会有疑问,context.read<ProgressBarController>()context.watch<ProgressBarController>()有什么区别呢?

我们将上面代码中的watch改为read,会发现点击按钮时value的值没有更新。到这里答案已经很明显了,使用watch方法获取ProgressBarController的实例,调用次方法的widget会监听ProgressBarController的状态变化,而read仅仅是获取ProgressBarController的实例,并不会监听状态的改变。

既然如此能不能都用watch呢?答案是不建议这样使用,因为这样会造成不必要的性能消耗。

看看read和watch的源码,其实两者都是对Provider.of<T>(),方法的封装。

context.read<ProgressBarController> 相当于Provider.of<ProgressBarController>(context,listen:false);

context.watch<ProgressBarController>()相当于Provider.of<ProgressBarController>(context,listen:true);

读者可以自行验证。

1.5Consumer方法

上节说到,通过context.watch<ProgressBarController>()可以监听Model层数据从而更新界面。这种写法有时候显得太繁琐,不够直观。为此Provider封装了一系列Consumer方法,用于获取状态。具体用法为:

Consumer<ProgressBarController>{
    builder:(){
        // ...
    }
}

在builder方法中返回用于展示进度的控件,直观又简洁。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值