一、什麼是全域性狀態管理
當我們在使用 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 程式設計開發」微信公眾號 。