Flutter Provider状态管理-基本认识、示例

Author Avatar
Amos
发表:2021-12-20 09:50:23
修改:2021-12-23 11:50:23

“状态(State)管理” 在响应式编程中都是非常重要的存在,无论是 Flutter,还是 Vue、React(Web开发框架),在思想上其实都是一致的。

我最近一段时间都在啃 Google 的 Flutter,学习路径到状态管理的地方,正好 写一篇文章巩固一下(算是重复造轮子,因为市面上相关教学文章已经非常丰富了,我在这里主要是写上自己的理解和示例,用以巩固)。

目前在社区内 ProviderBLoCGetXMobXRedux 等,都是受欢迎的状态管理包。
我经过实际使用对比筛选多个库,最终选择了较好上手并且入选 Flutter Favorite 受到官方推荐的 Provider 进行使用(并非官方出品)。

环境

本文基础建立在空安全支持、Flutter 2.8、Dart 2 的版本下使用。
以下是我当前使用的环境:

[√] Flutter (Channel stable, 2.8.0, on Microsoft Windows [Version 10.0.22000.318], locale zh-CN)
[√] Dart SDK version: 2.15.1 (stable) on “windows_x64”
[√] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
[√] Android Studio (version 2020.3)
[√] VS Code (version 1.63.1)

Packages

dependencies:
    provider: ^6.0.1

为什么要使用状态管理

这一直是一个很长久的话题,已经了解的话可以跳过这个部分。

就像下面这张图一样,在项目初期或者是简单构建的应用,也许直接使用数据进行视图渲染就可行了。


[图片]

模拟简单结构(翻译为个人理解,有问题请指出)


但是随着长时间的迭代或是构建庞大的项目,现实情况会存在多个组件共享一个状态或是组件之间多层的嵌套等等,过多的数据状态和业务逻辑会使整体结构变得零散且难以管理,也会使得代码的可读性和可维护性大大降低。


[图片]

模拟复杂结构(翻译为个人理解,有问题请指出)


所以我们在需要的时候可以将业务逻辑和数据进行抽离,以方便我们更好地进行管理。

Provider优势

ProviderInheritedWidget 组件的上层封装,使其更易用,更易复用。
使用 Provider 而非手动书写 InheritedWidget,有以下的优势:

  • 简化的资源分配与处置
  • 懒加载
  • 创建新类时减少大量的模板代码
  • 支持 DevTools
  • 更通用的调用 InheritedWidget 的方式(参考 Provider.of / Consumer / Selector
  • 提升类的可扩展性,整体的监听架构时间复杂度以指数级增长(如 ChangeNotifier, 其复杂度为 O(N))

——引用自Provider官方

详细的 Provider 文档:链接

名称 描述
Provider 最基础的 provider 组成,接收一个任意值并暴露它。
ListenableProvider 供可监听对象使用的特殊 provider。ListenableProvider 会监听对象,并在监听器被调用时更新依赖此对象的 widgets。
ChangeNotifierProvider 为 ChangeNotifier 提供的 ListenableProvider 规范,会在需要时自动调用 ChangeNotifier.dispose。
ValueListenableProvider 监听 ValueListenable,并且只暴露出 ValueListenable.value。
StreamProvider 监听流,并暴露出当前的最新值。
FutureProvider 接收一个 Future,并在其进入 complete 状态时更新依赖它的组件。
······ ······

基础示例

接下来会对 Provider 的基础用法进行实例使用,实现在两个页面的计数器效果,让我们有个简单的认识。
为了思维能够连贯清晰,我将所有代码都贴了出来(可以直接拷贝到项目中测试),文章篇幅会因此变长,阅读过程只需要关注代码中和 Provider 相关的部分。


[图片]

计数器简单示例


示例基本结构

├── lib    
│   ├── view_models                     # 视图模型
│   │   └── counter_view_model.dart     # Counter视图模型
│   ├── views                           # 视图
│   │   ├── counter_operation_view.dart # CounterOperation视图
│   │   └── counter_view.dart           # Counter视图
│   └── main.dart                       # 主入口

一、创建ViewModel视图模型

counter_view_model.dart

import 'package:flutter/material.dart';

class CounterViewModel with ChangeNotifier {
  int _count = 0;

  void increment() {
    _count++;
    notifyListeners();
  }

  void decrement() {
    if (_count > 0) {
      _count--;
    }
    notifyListeners();
  }

  int get count => _count;
}

将具体业务逻辑和数据状态抽离到视图模型中,mixin 混入 ChangeNotifier 类,帮助我们监听模型中的状态变化,根据需要在方法中调用 notifyListeners() 主动通知渲染视图。
因 Flutter 只有当树中的数据与传入的新数据不同时才会更新,所以在数据不改变的情况下多次调用 notifyListeners() ,也只会渲染一次视图。
将私有变量 _count 通过 get 暴露出去,并且提供了两个计算方法 increment()decrement()

二、挂载根节点状态共享

main.dart

import 'package:flutter/material.dart';
/// packages
import 'package:provider/provider.dart';
/// view_models
import 'package:flutter_demo_1/view_models/counter_view_model.dart';
/// views
import 'package:flutter_demo_1/views/counter_view.dart';
import 'package:flutter_demo_1/views/counter_operation_view.dart';

void main() {
  runApp(const CounterApp());
}

class CounterApp extends StatelessWidget {
  const CounterApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    /// Provider
    return ChangeNotifierProvider(
      create: (_) => CounterViewModel(),
      child: MaterialApp(
        title: 'Provider',
        initialRoute: "/counter",
        routes: {
          '/counter': (context) => const CounterView(),
          '/counter_operation': (context) => const CounterOperationView(),
        },
      ),
    );
  }
}

这是我们最顶层的入口,使用 ChangeNotifierProvider 将我们的 CounterViewModel 挂载到根节点为子节点进行状态共享(这里用作演示挂载到最顶层入口全局处,现实情况千万不能因为偷懒所有都挂载到最顶层根节点,一定要根据需求判断是进行局部挂载还是全局挂载,否则会影响应用的性能)。

这里的 ChangeNotifierProvider 作为 “提供者” 之一,他会监听模型状态的变化,当数据变化时,他会通知 “消费者” 进行重建,在需要时会自动清理资源。

如果需要挂载多个模型,我们还可以利用 MultiProvider 使我们的结构更方便阅读 ,比如下面这样:

return MultiProvider(
  providers: [
    ChangeNotifierProvider<Model1>(create: (_) => Model1()),
    ChangeNotifierProvider<Model2>(create: (_) => Model2()),
    /// 更多...
  ],
  child: MaterialApp(
    home: Example(),
  ),
);

三、Counter页渲染数据

counter_view.dart

import 'package:flutter/material.dart';
/// packages
import 'package:provider/provider.dart';
/// view_models
import 'package:flutter_demo_1/view_models/counter_view_model.dart';

class CounterView extends StatelessWidget {
  const CounterView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        backgroundColor: Colors.black,
        child: const Icon(Icons.arrow_upward),
        onPressed: () {
          Navigator.pushNamed(context, '/counter_operation');
        },
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
      body: const SafeArea(
        child: Counter(),
      ),
    );
  }
}

class Counter extends StatelessWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: SizedBox(
        height: 128.0,
        width: 128.0,
        child: DecoratedBox(
          decoration: BoxDecoration(
            gradient: const LinearGradient(
              colors: [
                Color(0xFFE3B2CB),
                Color(0xFFE3B2CB),
              ],
            ),
            borderRadius: BorderRadius.circular(36.0),
          ),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text(
                "Count",
                style: TextStyle(
                  fontSize: 16.0,
                ),
              ),

              /// Provider
              Consumer<CounterViewModel>(
                builder: (_, CounterViewModel counterViewModel, child) {
                  int _count = counterViewModel.count;
                  return Text(
                    _count.toString(),
                    style: const TextStyle(
                      fontSize: 48.0,
                      fontWeight: FontWeight.bold,
                    ),
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

这是我们计数器的首页,状态数据需要渲染的页面使用 “消费者” 之一的 Consumer,合理使用他能够将渲染粒度降低到只需要视图刷新的部分,复杂场景能有效提高应用性能,在实际使用场景非常常用,Consumer2-6 最多能同时装6个状态模型。

Consumer 里面有三个属性:

context: 当前的上下文
T: 需要的根节点模型对象
child: 子组件(不需要刷新的部分)

除此之外 “消费者” 还有 Provider.of(context)SelectorInheritedContext系列,在实际使用中都需要按照他们的特性结合进行使用,以达到最好疗效。

四、CounterOperation页触发方法

counter_operation_view.dart

import 'package:flutter/material.dart';
/// packages
import 'package:provider/provider.dart';
/// view_models
import 'package:flutter_demo_1/view_models/counter_view_model.dart';

class CounterOperationView extends StatelessWidget {
  const CounterOperationView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        backgroundColor: Colors.black,
        child: const Icon(Icons.arrow_downward),
        onPressed: () {
          Navigator.of(context).pop();
        },
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
      body: const SafeArea(
        child: CounterOperation(),
      ),
    );
  }
}

class CounterOperation extends StatelessWidget {
  const CounterOperation({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          CounterButton(
            icon: const Icon(Icons.add),
            onPressed: () {
              /// Provider
              Provider.of<CounterViewModel>(context, listen: false).increment();
            },
          ),
          CounterButton(
            icon: const Icon(Icons.remove),
            onPressed: () {
              /// Provider
              Provider.of<CounterViewModel>(context, listen: false).decrement();
            },
          ),
        ],
      ),
    );
  }
}

class CounterButton extends StatelessWidget {
  const CounterButton({Key? key, required this.icon, required this.onPressed})
      : super(key: key);

  final Widget icon;
  final Function() onPressed;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 64.0,
      width: 64.0,
      margin: const EdgeInsets.all(8.0),
      child: DecoratedBox(
        decoration: BoxDecoration(
          gradient: const LinearGradient(
            colors: [
              Color(0xFFE3B2CB),
              Color(0xFFE3B2CB),
            ],
          ),
          borderRadius: BorderRadius.circular(24.0),
        ),
        child: Material(
          color: Colors.transparent,
          child: IconButton(
            splashColor: Colors.black26,
            icon: icon,
            onPressed: onPressed,
          ),
        ),
      ),
    );
  }
}

这是第二个页面用于触发业务逻辑,主要简单模拟跨组件触发业务逻辑改变数据状态,利用最简单的 “消费者” Provider.of(context) 触发我们的业务方法,比如触发 CounterViewModel 中的增加方法 increment(),这里对模型内部中私有的 _count 进行累加,并通过 notifyListeners() 主动通知渲染视图,也就达到了我们在其他组件或页面也能够更方便的进行共享调用的目的。

我们也可以在使用中利用 Provider 给我们提供的语法糖 InheritedContext系列 替换掉 Provider.of(context) 进行使用。
比如 InheritedContext系列 其中之一的 BuildContext.watch ,让我们可以不再使用 Consumer,就像下面这样:

@override
Widget build(BuildContext context) {

  /// BuildContext.watch
  final counterViewModel = context.watch<CounterViewModel>();

  return ElevatedButton(
    onPressed: () => counterViewModel.increment(),
    child: Text("+1"),
  );
);

总结

本文主要介绍了 Provider 的基础认识和部分 Provider 的常见用法,相对于原生提供的 InheritedWidget 显然 Provider 更便于使用。
示例演示将计数器业务逻辑和数据状态抽离至 视图模型,使用了 ChangeNotifierProvider 对入口根节点挂载了单个视图模型,利用 Consumer 监听并局部渲染显示数据,简单使用 Provider.of(context) 触发业务方法并通知改变渲染,在实际使用中也可以利用 MVVM 的设计思想为项目提供更好的状态管理。

相关

[1]Flutter完整开发实战详解(十五、全面理解State与Provider)
[2]Flutter | 状态管理指南篇——Provider

转载请遵循 协议许可
本文所有内容严禁任何形式的盗用
本文作者:Amos Amos
本文链接:https://amoshk.top/2021122001/

评论
✒️ 支持 Markdown 格式
🖼️ 头像与邮箱绑定 Gravatar 服务
📬 邮箱会回复提醒(也许会在垃圾箱内)