【Android】从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"),
            ),
          ],
        ));
  } 

运行结果如下:

微信截图_20220208152753.png

可以看到有两个按钮,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()方法。该方法会寻找离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实例,然后调用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()context.watch() 有什么区别呢?

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

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

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

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

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

1.5Consumer方法

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

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

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

最后

为了帮助大家更好的理解flutter,我给大家准备了一份《Flutter进阶学习笔记》,相信大家能在它的帮助下快速掌握flutter的知识,有需要的朋友可以扫码自取

《Flutter进阶学习笔记》

目录

第一章 为什么 Flutter 是跨平台开发的终极之选

  • 这是为什么?
  • 跨平台开发
  • 什么是Flutter
  • Flutter特性
  • Flutter 构建应用的工具
  • 使用 Flutter 构建的热门应用
  • 构建 Flutter 应用的成本

第二章 在Windows上搭建Flutter开发环境

  • 使用镜像
  • 系统要求
  • 获取Flutter SDK
  • 编辑器设置
  • Android设置
  • 起步: 配置编辑器
  • 起步: 体验
  • 体验热重载

第三章 编写您的第一个 Flutter App

  • 创建 Flutter app
  • 使用外部包(package)
  • 添加一个 有状态的部件(Stateful widget)
  • 创建一个无限滚动ListView
  • 添加交互
  • 导航到新页面
  • 使用主题更改UI

第四章 Flutter开发环境搭建和调试

  • 开发环境的搭建
  • 模拟器的安装与调试
  • 开发环境的搭建
  • 模拟器的安装与调试

第五章 Dart语法篇之基础语法(一)

  • 简述
  • Hello Dart
  • 数据类型
  • 变量和常量
  • 集合(List、Set、Map)
  • 流程控制
  • 运算符
  • 异常
  • 函数
  • 总结

第六章 Dart语法篇之集合的使用与源码解析(二)

  • List
  • Set
  • Map
  • Queue
  • LinkedList
  • HashMap
  • Map、HashMap、LinkedHashMap、SplayTreeMap区别
  • 命名构造函数from和of的区别以及使用建议

第七章 Dart语法篇之集合操作符函数与源码分析(三)

  • 简述
  • Iterable
  • forEach
  • map
  • any
  • every
  • where
  • firstWhere和singleWhere和lastWhere
  • join
  • take
  • takeWhile
  • skip
  • skipWhile
  • follwedBy
  • expand
  • reduce
  • elementAt

第八章 Dart语法篇之函数的使用(四)

  • 简述
  • 函数参数
  • 匿名函数(闭包,lambda)
  • 箭头函数
  • 局部函数
  • 顶层函数和静态函数
  • main函数
  • Function函数对象

第九章 Dart语法篇之面向对象基础(五)

  • 简述
  • 属性访问器(accessor)函数setter和getter
  • 面向对象中的变量
  • 构造函数
  • 抽象方法、抽象类和接口
  • 类函数
  • 总结

第十章 Dart语法篇之面向对象继承和Mixins(六)

  • 简述
  • 类的单继承
  • 基于Mixins的多继承
  • 总结

第十一章 Dart语法篇之类型系统与泛型(七)

  • 简述
  • 可选类型
  • 接口类型
  • 泛型
  • 类型具体化
  • 总结

第十二章 Flutter中的widget

  • Flutter页面-基础Widget
  • Widget
  • StatelessWidget
  • State生命周期
  • 基础widget
  • DefaultTextStyle
  • FlutterLogo
  • Icon
  • Iamge.asset
  • CircleAvatar
  • FadeInImage
  • 按钮
  • FlatButton
  • OutlineButton
  • TextFormField

后话:

现在工具的更新迭代速度之快,尤其是Android开发工程师,必须不断学习最新的工具和方法,才能够适应Android项目实战的变化,所以赶紧把flutter学习起来吧,加油!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值