一、什么是全局状态管理
当我们在使用 Flutter 进行应用开发时,可能需要不同的页面共享应用或者说变量的状态,当这个状态发生改变时,所有依赖这个状态的 ui 都会随之发生改变。在同一个页面中还好说,直接通过 setState 就可以达到目的,要是不同的页面呢,或者当应用变得非常复杂,页面非常多的时候,这个时候全局状态管理就显得非常重要了。
在 Flutter 中,状态管理可以有如下几种方式:
1、setState
flutter 中最简单使 ui 根据状态发生改变的方式。
2、 InheritedWidget & InheritedModel
InheritedWidget 和 InheritedModel 是 flutter 原生提供的状态管理解决方案。 当InheritedWidget发生变化时,它的子树中所有依赖了它的数据的Widget都会进行rebuild,这使得开发者省去了维护数据同步逻辑的麻烦。
3、Provider & Scoped Model
Provider 与 Scoped Model 都属于第三方库,两者使用起来差不多,其中 Provider 是 Google I/O 2019 大会上官方推荐的状态管理方式。
4、Redux
在 Redux 状态管理中,所有的状态都储存在Store里,Flutter 中的 Widget 会根据这个 Store 去渲染视图,而状态的改变也是通过 Reduex 里面的 action 来进行的。
5、BLoC / Rx
BLoC的全称是 业务逻辑组件(Business Logic Component)。就是用reactive programming方式构建应用,一个由流构成的完全异步的世界。 BLoc 可以看作是 Flutter 中的异步事件总线,当然在除了 BLoc 外,Flutter 中有专门的响应式编程库,就是RxDart,RxDart是基于ReactiveX标准API的Dart版本实现,由Dart标准库中Stream扩展而成。
二、Provider 介绍和使用
1、Provider 是什么
Provider 是 Google 官方推荐的状态管理解决方案,本质上也是使用 InheritedWidget 来进行状态管理的,所以也可以理解为 Provider 是 Flutter 中的语法糖,主要是对 InheritedWidget 的封装方便我们的使用。
Provider 使用起来非常方便,访问数据的方式有两种,无论是获取状态还是更新状态,都是这两种:
- Provider.of(context)
- Consumer
2、Provider 基本使用
接下来直接参考官方的 Demo 了。
1、要添加依赖:
provider: ^3.1.0
2、定义数据 model
class Counter with ChangeNotifier {
///这个 model 只管理一个变量。
int value = 0;
///操作变量
void increment() {
value += 1;
notifyListeners();
}
}
3、使用 ChangeNotifierProvider 进行数据管理
ChangeNotifierProvider(
// Initialize the model in the builder. That way, Provider
// can own Counter's lifecycle, making sure to call `dispose`
// when not needed anymore.
///builder 会指定数据 model 并初始化。
builder: (context) => Counter(),
child: MyApp(),
),
4、监听状态改变可以使用 Provider.of 或者 Consumer
Consumer<Counter>(
builder: (context, counter, child) => Text(
'${counter.value}',
style: Theme.of(context).textTheme.display1,
),
),
Text('使用 Provider.of 方式 获取 model:'),
Text('${_counter.value}',),
5、改变数据,同样可以使用 Provider.of 或者 Consumer
floatingActionButton: FloatingActionButton(
/// listen 为 false 表示不监听状态改变,默认时 true
onPressed: () => Provider.of<Counter>(context, listen: false).increment(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
///需要修改 Model 同样可以使用 Consumer 的方式
// floatingActionButton: Consumer<Counter>(
// builder: (context, Counter counter, child) => FloatingActionButton(
// onPressed: counter.increment,
// child: child,
// ),
// child: Icon(Icons.add),
// ),
完整代码:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
///使用 ChangeNotifierProvider ,这个 Provider 将数据 model 粘合在一起,数据改变时,保证 MyApp 或者其子 Widget ui 更新。
ChangeNotifierProvider(
// Initialize the model in the builder. That way, Provider
// can own Counter's lifecycle, making sure to call `dispose`
// when not needed anymore.
///builder 会指定数据 model 并初始化。
builder: (context) => Counter(),
child: MyApp(),
),
);
}
/// Simplest possible model, with just one field.
///
/// [ChangeNotifier] is a class in `flutter:foundation`. [Counter] does
/// _not_ depend on Provider.
///
///
class Counter with ChangeNotifier {
///这个 model 只管理一个变量。
int value = 0;
///操作变量
void increment() {
value += 1;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
///通过 Provider.of 方式获取 model
final _counter = Provider.of<Counter>(context);
return Scaffold(
appBar: AppBar(
title: Text('Flutter Demo Home Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('使用 Consumer 获取 model:'),
// Consumer looks for an ancestor Provider widget
// and retrieves its model (Counter, in this case).
// Then it uses that model to build widgets, and will trigger
// rebuilds if the model is updated.
///Consumer 回向上寻找 Provider 类型的父类 Widget,并且取出 Provider 关联的 Model,根据这个 model 来构建 widget
///并且当 model 数据发生改变时,回触发更新。
///
Consumer<Counter>(
builder: (context, counter, child) => Text(
'${counter.value}',
style: Theme.of(context).textTheme.display1,
),
),
Text('使用 Provider.of 方式 获取 model:'),
Text('${_counter.value}',),
],
),
),
floatingActionButton: FloatingActionButton(
/// listen 为 false 表示不监听状态改变,默认时 true
onPressed: () => Provider.of<Counter>(context, listen: false).increment(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
///需要修改 Model 同样可以使用 Consumer 的方式
// floatingActionButton: Consumer<Counter>(
// builder: (context, Counter counter, child) => FloatingActionButton(
// onPressed: counter.increment,
// child: child,
// ),
// child: Icon(Icons.add),
// ),
);
}
}
效果:
3、多个页面数据共享
接下来看一下如何在不同的页面中共享数据做到全局状态管理。
还是以官方 Demo 为例说明。 假设有一个购物应用程序,有一个购物车页面和一个结账页面,购物车页面添加商品,结账页面可以看到所有添加的商品,对商品来说,就是共享的数据源。 先看一下效果:
1、定义数据 model
在这个示例中,有两个 model,一个是商品模型,一个是购物车,购物车存放选择的商品。
- 商品 Model
/// A proxy of the catalog of items the user can buy.
///
/// In a real app, this might be backed by a backend and cached on device.
/// In this sample app, the catalog is procedurally generated and infinite.
///
/// For simplicity, the catalog is expected to be immutable (no products are
/// expected to be added, removed or changed during the execution of the app).
class CatalogModel {
static const _itemNames = [
'Code Smell',
'Control Flow',
'Interpreter',
'Recursion',
'Sprint',
'Heisenbug',
'Spaghetti',
'Hydra Code',
'Off-By-One',
'Scope',
'Callback',
'Closure',
'Automata',
'Bit Shift',
'Currying',
];
/// Get item by [id].
///
/// In this sample, the catalog is infinite, looping over [_itemNames].
Item getById(int id) => Item(id, _itemNames[id % _itemNames.length]);
/// Get item by its position in the catalog.
Item getByPosition(int position) {
// In this simplified case, an item's position in the catalog
// is also its id.
return getById(position);
}
}
@immutable
class Item {
final int id;
final String name;
final Color color;
final int price = 42;
Item(this.id, this.name)
// To make the sample app look nicer, each item is given one of the
// Material Design primary colors.
: color = Colors.primaries[id % Colors.primaries.length];
@override
int get hashCode => id;
@override
bool operator ==(Object other) => other is Item && other.id == id;
}
只是简单的模拟一下用户选择的商品,包含 id,name,color,price 这四个字段。
- 购物车 Model
class CartModel extends ChangeNotifier {
/// The current catalog. Used to construct items from numeric ids.
final CatalogModel _catalog;
/// 购物车中存放商品的 list,只存 id 就行
final List<int> _itemIds;
/// Construct a CartModel instance that is backed by a [CatalogModel] and
/// an optional previous state of the cart.
///
/// If [previous] is not `null`, it's items are copied to the newly
/// constructed instance.
CartModel(this._catalog, CartModel previous)
: assert(_catalog != null),
_itemIds = previous?._itemIds ?? [];
/// 将存放商品 id 的数组转换为存放商品的数值,函数式编程。
List<Item> get items => _itemIds.map((id) => _catalog.getById(id)).toList();
/// 获取价格总和,dart 中的 List 中有两个累加的方法 reduce 和 fold,fold 可以提供一个初始值。
int get totalPrice =>
items.fold(0, (total, current) => total + current.price);
///添加商品,这个方法时外界可以修改 list 的唯一途径
void add(Item item) {
_itemIds.add(item.id);
// This line tells [Model] that it should rebuild the widgets that
// depend on it.
notifyListeners();
}
}
商品中通过 List 存放选择的商品。这里的购物车 Model 实现的是 ChangeNotifier,做为可改变的数据源。对于不同类型的可改变数据源,Provider 提供了不同的类提供我们选择,常见的有如下几种:
2、购物车页面提供商品选择,改变数据状态
class MyCatalog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
_MyAppBar(),
///上间距
SliverToBoxAdapter(child: SizedBox(height: 12)),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _MyListItem(index)
,
childCount: 25 ///本来时无限加载的,这里加上数量限制。
),
),
],
),
);
}
}
class _AddButton extends StatelessWidget {
final Item item;
const _AddButton({Key key, @required this.item}) : super(key: key);
@override
Widget build(BuildContext context) {
///通过 Provider.of 方式使用 CartModel
var cart = Provider.of<CartModel>(context);
return FlatButton(
///判断是否为空,不为空 list 中添加 item
onPressed: cart.items.contains(item) ? null : () => cart.add(item),
splashColor: Theme.of(context).primaryColor,
child: cart.items.contains(item)
? Icon(Icons.check, semanticLabel: 'ADDED')
: Text('ADD'),
);
}
}
class _MyAppBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SliverAppBar(
title: Text('Catalog', style: Theme.of(context).textTheme.display4),
floating: true,
actions: [
IconButton(
icon: Icon(Icons.shopping_cart),
onPressed: () => Navigator.pushNamed(context, '/cart'),
),
],
);
}
}
class _MyListItem extends StatelessWidget {
final int index;
_MyListItem(this.index, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
///Provider.of 方式获取 model
var catalog = Provider.of<CatalogModel>(context);
var item = catalog.getByPosition(index);
var textTheme = Theme.of(context).textTheme.title;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: LimitedBox(
maxHeight: 48,
child: Row(
children: [
AspectRatio(
aspectRatio: 1,
child: Container(
color: item.color,
),
),
SizedBox(width: 24),
Expanded(
child: Text(item.name, style: textTheme),
),
SizedBox(width: 24),
_AddButton(item: item),
],
),
),
);
}
}
3、购物车页面,获取数据
class MyCart extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Cart', style: Theme.of(context).textTheme.display4),
backgroundColor: Colors.white,
),
body: Container(
color: Colors.yellow,
child: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(32),
child: _CartList(),
),
),
Divider(height: 4, color: Colors.black),
///价格
_CartTotal()
],
),
),
);
}
}
class _CartList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var itemNameStyle = Theme.of(context).textTheme.title;
///使用 Provider.of 方式获取 CartModel
var cart = Provider.of<CartModel>(context);
return ListView.builder(
itemCount: cart.items.length,
itemBuilder: (context, index) => ListTile(
leading: Icon(Icons.done),
title: Text(
cart.items[index].name,
style: itemNameStyle,
),
),
);
}
}
class _CartTotal extends StatelessWidget {
@override
Widget build(BuildContext context) {
var hugeStyle = Theme.of(context).textTheme.display4.copyWith(fontSize: 48);
return SizedBox(
height: 200,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
///使用 Consumer 方式使用 CartModel
Consumer<CartModel>(
builder: (context, cart, child) =>
Text('\?{cart.totalPrice}', style: hugeStyle)),
SizedBox(width: 24),
FlatButton(
onPressed: () {
Scaffold.of(context).showSnackBar(
SnackBar(content: Text('Buying not supported yet.')));
},
color: Colors.white,
child: Text('BUY'),
),
],
),
),
);
}
}
参考:
https://medium.com/flutter-community/flutter-statemanagement-with-provider-ee251bbc5ac1
https://flutter.cn/docs/development/data-and-backend/state-mgmt/simple
https://pub.dev/packages/provider#-readme-tab-
https://pub.dev/documentation/provider/latest/provider/provider-library.html
最后
欢迎关注「Flutter 编程开发」微信公众号 。