Flutter 状态管理探索

370 阅读11分钟

状态管理

状态管理是前端开发领域上常用的一门技术,前端开发一直很注重 UI 和业务数据的独立性,即数据源的唯一性和数据的单向流动性。

在移动开发领域,比如iOS, 一开始是以 MVC 框架来作为 UI 和 业务数据的构建,并没有状态管理相应的概念,分别是 Model、Controller 和 View:

  • 用户通过 View 上的 Action 触发逻辑回调给 Controller
  • 通过 Controller 去更新 Model 中的数据
  • Model 数据更新后,通知 View 刷新 UI

MVC 如果按规范使用起来,其实也可以实现数据源的单一和数据的单向流动性,但实际应用中却存在诸多的不利因素:

  • 缺乏明确的定义和规范
  • Controller 除了管理 Views,还要管理 Models,到后期会过于臃肿
  • Controller 和 View 的关联性过强
  • 等等

而 Flutter,在一推出的时候就引入状态管理来,通过这强有力状态管理来实现数据源的单一性和数据流的单向性。

状态管理

那什么才是状态管理?

状态管理,是用单一数据流的思想指导整个系统,把状态存储在特定的地方,在 UI 组件层监听数据的改变,并刷新 UI,通过 Action 触发数据更新。

状态管理的优点

  • 能有效分离 UI 层和数据处理层
  • 有效控制状态变化和 UI 的刷新
  • 在业务组织上更加解耦
  • 代码维护性和稳定性更高

Flutter 的状态管理方案

Flutter 作为一个响应式框架,状态管理是 Flutter 开发过程中代码架构的重点,官方和开发者提供了很多 Flutter 状态管理相关的优秀开源库,不同的技术方案有不同的落地场景。

State 和 InheritedWidget

State 和 InheritedWidget 是 Flutter 中最基础的状态管理方案。

在 Flutter 中,Widget 是不可变的,但 State 是支持跨帧保存信息,当开发者执行 setState 方法时,State 内部会通过调用 markNeedBuild 方法,将 State 对应的 Element 标记为 diry,从而下一帧会执行 WidgetsBinding.drawFrame 时更新界面信息。

InheritedWidget 在 Flutter 中用于数据共享,可以实现数据从上往下传递共享,child 通过 BuildContext 可以往上获取 InheritedWidget 中的数据。比如:Material库里面的主题管理(Theme), Scoped Model, Redux 都是使用这个机制来实现数据共享。

通过继承 InheritedWidge,用于绑定依赖和控制刷新通知:

class InheritedText extends InheritedWidget {
  final String text;
  const InheritedText({Key? key, required this.text, required Widget child})
      : super(child: child, key: key);

  //该回调决定当data发生变化时,是否通知子树中依赖data的Widget
  @override
  bool updateShouldNotify(covariant InheritedText oldWidget) {
    return text != oldWidget.text;
  }

  //定义一个便捷方法,方便子树中的widget获取共享数据
  static InheritedText? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<InheritedText>();
  }
}

实现一个子组件 TextWidget,在 build 方法中引用 InheritedText 中的数据:

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

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      width: 200,
      height: 100,
      color: Colors.redAccent,
      child: Text(
        InheritedText.of(context)?.text ?? '',
      ),
    );
  }
}

最后创建一个按钮,更新数据:

class _StateManagerPageState extends State<StateManagerPage> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: SafeArea(
        child: Center(
          child: InheritedText(
            text: _count.toString(),
            child: const TextWidget(),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          setState(() {
            _count++;
          });
        },
      ),
    );
  }
}

Stream

Stream 在 Flutter 中也是经常使用到,但它却不是 Flutter 设计的,而是来自 Dart 的封装,一般它和 Future 用于实现异步的逻辑。

Future 一般我们常用于单个异步实现,而 Stream 是用于多个异步实现,以数据流的方式存在,不仅如此,Stream 可以支持同步或异步。

在 Flutter 的状态管理设计中,除了 State 和 InheritedWidget,其他的像 rxdart、BLoc、flutter_redux、Provider 都离不开 Stream 的封装。

  • 在Flutter的Stream流中,对外暴露的对象主要有StreamController、Sink、Stream和StreamSubscription
  • StreamController:如类名描述,用于控制整个Stream的过程,提供各类接口用于创建各种事件流。
  • StreamSink:一般作为事件的入口,提供如add、addStream等方法。
  • Stream:事件源本身,一般可用于监听事件流或者对事件进行转换,如 listen、where等方法。
  • StreamSubscription:事件订阅后得到的对象,表面上用于管理订阅过的各类操作,如cancel、pause等操作,同时在事件流的内部也是事件中转的关键。

Stream 的广播和非广播

在 Stream 中有广播 _BroadcastStreamController 和 非广播 _StreamController 区分:

  • 非广播只允许设置一个监听器,并且只有在 Stream 设置监听器才开始产生事件,取消监听器则停止发送事件,并且也不允许在流上设置其他监听器
  • 广播则允许设置多个监听器

Stream 的事件转换

Stream 支持对事件进行变换的处理,通过变化可以经过筛选和多次处理,从而最终达到需要的结果,如 map、where、elementAt 等。

Stream 的简单使用

Stream的使用方式:

  1. 首先创建StreamController对象
  2. 通过StreamController获取StreamSink对象用于事件入口
  3. 通过StreamController获取Stream对象用于监听数据变化
  4. 通过Stream的listen监听得到StreamSubscription对象用于管理订阅

创建 StreamController,StreamController 有多个生命周期,如 cancel、listen、pause、resume 等:

final StreamController<int> _streamController = StreamController(
    onCancel: () {
      debugPrint('_streamController onCancel');
    },
    onListen: () {
      debugPrint('_streamController onListen');
    },
    onPause: () {
      debugPrint('_streamController onPause');
    },
    onResume: () {
      debugPrint('_streamController onResume');
    },
    sync: true,
);

通过 StreamSubscription 对 Stream 数据流进行监听,并刷新 UI:

_streamSubscription = _streamController.stream.listen((event) {
      setState(() {
        _count += event;
      });
});

通过 Sink 添加新的数据:

_streamController.sink.add(1);

RxDart

从订阅或者变换的角度可以看出,Stream 都已经拥有了 ReactiveX 的设计思想,为了让开发者更加方便的使用 Stream,ReactiveX 为 Dart 语言封装了 RxDart 来满足对 ReactiveX 的熟悉感,所以 RxDart 是 Stream 的拓展。

DartRxDart
StreamControllerSubject
StreamObservable

在RxDart,Observable是一个Stream,而Subject继承了Observable,所以也是一个Stream,并且还实现了StreamController的接口,所以具备Controller和Stream的两种作用。

RxDart 的使用流程总结:

  • PublishSubject 内部创建了一个广播 StreamController.broadcast,add 或者 addStream 最终都会调用 StreamController.add
  • 当调用 onListen 时,也是将回调设置回 StreamController 中
  • RxDart 在做变换时,也是基于创建时传入的 Stream 对象进行变换

RxDart 只是对 Stream 进行了概念的转换,本身还是 Stream。

首先创建一个被观察者 Subject:

final PublishSubject<int> _subject = PublishSubject<int>();

通过 StreamSubscription 对 Subject 数据流进行监听,并刷新 UI:

_streamSubscription = _subject.stream.listen((event) {
    setState(() {
        _count += event;
    });
});

Subject 更新数据:

_subject.add(1);

scoped_model

scoped_model 是 Flutter 最为简单的第三方状态管理框架。

scoped_model 的使用

scoped_model 的实现状态管理的三步:

  1. 定义 Model 的实现,且在状态改变时执行 notifyListeners 方法
  2. 使用 ScopedModel 控件加载 Model
  3. 使用 ScopedModelDescendant 或者 ScopedModel.of(context) 加载 Model 内状态数据进行显示

首先创建一个 CountModel,继承 Model,并且实现一个 add 方法用于更新模型中的数据:

class CountModel extends Model {
  int _count = 0;

  int get count => _count;

  void add(int value) {
    _count += value;
    // 更新数据
    notifyListeners();
  }
}

使用 ScopedModel 控件初始化 CountModel,并且使用 ScopedModelDescendant 或 ScopedModel.of(context) 获取 Model 内容并显示:

return ScopedModel<CountModel>(
    model: CountModel(),
        child: Builder(
            builder: (context) {
            return Scaffold(
                appBar: AppBar(
                title: const Text('scoped_model'),
                ),
                body: const SafeArea(
                child: Center(child: TextWidget()),
                ),
                floatingActionButton: FloatingActionButton(
                onPressed: () {
                    var model = ScopedModel.of<CountModel>(context);
                    model.add(
                  Random().nextInt(10),
                    );
                },
                child: const Icon(Icons.add),
                ),
            );
        }
    ),
);

ScopedModel 的原理

  1. Model实现了Listenable接口,内部维护一个Set_listeners
  2. 当Model设置给AnimatedBuilder时,Listenable的addListener会被调用,然后添加一个_handleChange监听到 _listeners这个Set中
  3. 当Model调用notifyListeners,会通过异步调用scheduleMicrotask执行_listeners中的/_handleChange、
  4. _handleChange监听被调用,执行setState({})更新界面

Flutter_redux

redux 一般是前端开发常用的状态管理框架,在 redux 中,数据都存在单一的可信源中,叫 Store,然后数据储存时通过 Reducer 更新,而触发这些更新动作就是 Action。

  • Store 是数据存储仓库,保存所有数据,Flutter 一般建议只配置一个 Store
  • Action 代表一种操作,Redux 不允许世界操作 Store 里面的数据,Action 相当于一个 Store 请求
  • reducer 是真正修改 Store 的执行者,一般只有两个参数,一个是当前的 State,另一个是 Action
  • middleware 是中间件,一般可以拦截或做 AOP 等
  • 将 Store 设置给 StoreProvider 用于状态共享
  • 通过 StoreConnector 或 StoreBuilder 加载显示 Store 中的数据
class ReduxPage extends StatelessWidget {
  ReduxPage({Key? key}) : super(key: key);

  late final Store<CountState> _store = Store<CountState>(
      (state, action) => addReducer(state, action),
      initialState: CountState(count: 0),
      middleware: [CountMiddleware()]);

  @override
  Widget build(BuildContext context) {
    return StoreProvider(
      store: _store,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter redux 使用'),
        ),
        body: const SafeArea(
          child: CountWidget(),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return StoreBuilder<CountState>(builder: (context, countState) {
      String text = countState.state.count.toString();
      return Column(
        children: <Widget>[
          Expanded(
            child: Center(
              child: Text(text),
            ),
          ),
          Row(
            children: <Widget>[
              Expanded(
                child: FloatingActionButton(
                  onPressed: () {
                    countState.dispatch(Actions.addAction);
                  },
                  child: const Icon(Icons.add),
                ),
              ),
              Expanded(
                child: FloatingActionButton(
                  onPressed: () {
                    countState.dispatch(Actions.decAction);
                  },
                  child: const Icon(Icons.remove),
                ),
              )
            ],
          ),
        ],
      );
    });
  }
}

class CountState {
  CountState({required this.count});
  final int count;
}

// Reducer

CountState addReducer(CountState state, dynamic action) {
  if (action == Actions.addAction) {
    return CountState(count: _addHandler(state.count, action));
  } else {
    return CountState(count: _decHandler(state.count, action));
  }
}

int _addHandler(int count, Actions action) {
  count++;
  return count;
}

int _decHandler(int count, Actions action) {
  count--;
  return count;
}

enum Actions {
  addAction,
  decAction,
}

class CountMiddleware implements MiddlewareClass<CountState> {
  @override
  call(Store<CountState> store, action, NextDispatcher next) {
    if (action == Actions.addAction) {
      debugPrint('Actions.addAction');
    } else if (action == Actions.decAction) {
      debugPrint('Actions.decAction');
    }
    next(action);
  }
}

redux 的内部实现逻辑,是对 Stream、SteamBuilder 和 InheritedWidgets 的自定义封装。开发者每个操作都是一个 Action,而触发逻辑完全由 middleware 和 reducer 决定,并且整个流程是单向的,将业务和 UI 进行了隔离,规范了事件流过程中的规范。

Flutter Provider

Flutter Provider 是 Flutter 推荐的状态管理方式之一,特点是不复杂、好理解、可控度高。

Flutter Provider 包里面有很多 Provider 类型。

Provider

Provider 是最基本类型,它可以包括为所有子 Widget 提供值,但却不能更新 widget。

创建一个 CountModel:

class CountModel {
  int _count = 0;
  int get count => _count;

  void add(int value) {
    _count += value;
    //notifyListeners();
  }
}

接着创建一个 Provider,并初始化 model:

Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Provider'),
      ),
      body: SafeArea(
        child: Provider<CountModel>(
          create: (context) {
            return model;
          },
          child: const Center(child: TextWidget()),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          model.add(1);
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

虽然我们触发点击更新数值,但它是无法更新到 UI 的。

ChangeNotifierProvider

ChangeNotifierProvider 会监听提供出去的模型对象中的值更改,当值发生更改,会重建下方所有的 Consumer 和使用 Provider.of(context) 监听并获取值的地方。

首先 CountModel 需要继承 ChangeNotifier,并且在数据发生更改时要调用 notifyListeners 方法:

class CountModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void add(int value) {
    _count += value;
    notifyListeners();
  }
}

接着通过 ChangeNotifierProvider 包裹子 Widget,通过 Consumer 或 Provider.of(context) 监听数据更改和获取数据:

child: ChangeNotifierProvider<CountModel>(
    create: (context) {
        return model;
    },
    child: const Center(child: TextWidget()),
)

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

  @override
  Widget build(BuildContext context) {
    var model = Provider.of<CountModel>(context);
    return Container(
      alignment: Alignment.center,
      width: 200,
      height: 100,
      color: Colors.redAccent,
      child: Text(
        model.count.toString(),
      ),
    );
  }
}

FutureProvider

FutureProvider 是普通的 FutureBuilder 的包装,FutureProvider 初始化时需提供一个初始化值和延时值,当获取到延时值时会重新构建当前 UI,但却无法监听后续数据的更改。

FutureProvider 使用的 create 是一个返回 Future 类型的方法,可在里面进行延时请求,并需要提供初始数据:

child: FutureProvider(
    create: (context) async {
        await Future.delayed(const Duration(seconds: 2));
        return CountModel(count: 2);
    },
    initialData: CountModel(count: 1),
    child: const Center(child: TextWidget()),
),

StreamProvider

StreamProvider 是 StreamBuilder 的包装,和 FutureProvider 一样,无法监听模型数据的更改。

ValueListenableProvider

ValueListenableProvider 和 ChangeNotifierProvider 一样,在值改变的时候可以通知子 widget 重新构建 UI,但使用起来更加复杂,需要先用 Provider 提供 Model 给 Consumer。

首先构建一个 Model,Model 中有一个 ValueNotifier,提供给 ValueListenableProvider 使用:

class CountModel {
  ValueNotifier<int> counter = ValueNotifier(0);
}

接着需要先用 Provider 提供 Model,用 Consumer 包 ValueListenableProvider:

return Provider(
    create: (context) => CountModel(),
    child: Consumer<CountModel>(
        builder: (context, model, child) {
            return ValueListenableProvider<int>.value(
                value: model.counter,
                child: Scaffold(
                appBar: AppBar(
                    title: const Text('Provider'),
                ),
            body: Consumer<int>(
                builder: (context, value, child) {
                  return const SafeArea(
                    child: Center(child: TextWidget()),
                  );
                },
            ),
            floatingActionButton: Consumer<CountModel>(
                builder: (context, value, child) {
                  return FloatingActionButton(
                    onPressed: () {
                      model.counter.value += 1;
                    },
                    child: const Icon(Icons.add),
                  );
                },
              ),
            ),
          );
        },
      ),
    );
  }

ListenableProvider

ListenableProvider和ChangeNotifierProvider一样, 区别在于,如果Model是一个复杂模型ChangeNotifierProvider 会在你需要的时候,自动调用其 _disposer 方法,所以一般还是使用ChangeNotifierProvider即可。

MultiProvider

上面的这些例子都是用到单一模型,但一般在业务迭代,我们可能会用到多个模型,虽然可以通过嵌套来解决这样的需求,但嵌套毕竟多了有点恶心 MultiProvider 支持多个 providers 进行组合,解决嵌套问题。

声明 MultiProvider,并且在 providers 中放入多个 model:

return MultiProvider(
      providers: [
        ChangeNotifierProvider<BannerModel>(create: (context) => BannerModel()),
        ChangeNotifierProvider<ListModel>(create: (context) => ListModel()),
      ],
      ....
}

之后便可以同时监听多个 Model 的内容变化,从而驱动 UI 刷新。

ProxyProvider

如果要提供两个Model,但是其中一个Model取决于另一个Model,在这种情况下,可以使用ProxyProvider。A ProxyProvider从一个Provider获取值,然后将其注入另一个Provider,

比如我们上传图片会将图片先上传到阿里云等服务器,拿到 URL,再把 URL 传给自己的服务器:

return MultiProvider(
    providers: [
        ChangeNotifierProvider<PicModel>(create: (context) => PicModel()),
        ProxyProvider<PicModel, SubmitModel>(
          update: (context, myModel, anotherModel) => SubmitModel(myModel),
        ),
    ],
    ...
    Consumer<PicModel>(
        builder: (context, model, child) {
            return TextButton(
                onPressed: model.upLoadPic,
                child: const Text("提交图片"),
            );
        },
    ),
    Consumer<PicModel>(
        builder: (context, model, child) {
            return TextButton(
                onPressed: model.upLoadPic,
                child: const Text("提交图片"),
            );
        },
    ),
    ...
}

Flutter 状态管理的区别

scoped_model 比较适合新手和小型轻度的项目,而futter_redux、Provider更适合中大型项目。在挑选框架时,futter_redux更适合有前端开发经验的人,而Provider更适合拥有App开发经验的人使用。Provider、futter_redux 会更倾向于用在需要数据共享或者相对复杂的业务封装中,比如登录状态、用户信息、主题和多语言切换等。