Flutter狀態管理provider的使用和封裝
Flutter提供了InheritedWidget類,幫助我們處理父子元件之間的狀態管理。provider是InheritedWidget的封裝,讓開發者易於使用和服用。但是初看provider的文件,有點讓人頭大:
name | description |
---|---|
Provider | The most basic form of provider. It takes a value and exposes it, whatever the value is. |
ListenableProvider | A specific provider for Listenable object. ListenableProvider will listen to the object and ask widgets which depend on it to rebuild whenever the listener is called. |
ChangeNotifierProvider | A specification of ListenableProvider for ChangeNotifier. It will automatically call ChangeNotifier.dispose when needed. |
ValueListenableProvider | Listen to a ValueListenable and only expose ValueListenable.value. |
StreamProvider | Listen to a Stream and expose the latest value emitted. |
FutureProvider | Takes a Future and updates dependents when the future completes. |
不是說provider是易於使用嗎?我只想以一種的簡單的方式管理狀態,卻給我這麼多選擇,到底我該選擇哪個呢?選擇困難症急的想薅頭髮。
使用
新建Futter專案,更改預設的計數器佈局,效果如下:
點選FlatButton,更改應用程式的計數器狀態,使計數器加1,前兩行的text顯示計數器狀態最新值,FlatButton和兩個text是不同部分的widget。
- 在的pubspec.yaml檔案中依賴provider:
dependencies:
flutter:
sdk: flutter
provider: ^4.1.2
複製程式碼
- 匯入:
import 'package:provider/provider.dart';
Provider
Provider是provider包中最基本的提供者widget型別。它可以給包括住的所有widget提供值,但是當該值改變時,並不會更新widget。
新增MyModel類,作為要讓Provider提供出去的值,把計數器的數值counter宣告到這裡,並且更改計數值的方法也放在這裡,點選按鈕的時候,呼叫MyModel物件的incrementCounter(),延時2秒並更改counter:
class MyModel {
MyModel({this.counter=0});
int counter = 0;
Future<void> incrementCounter() async {
await Future.delayed(Duration(seconds: 2));
counter++;
print(counter);
}
}
複製程式碼
在widget樹的頂部包裹Provider小部件,將MyModel物件通過Provider提供給widget樹。然後使用了兩種獲取Provider提供值的方式,在Column裡:
- 先使用Provider.of(context)獲取到MyModel物件的引用;
- 然後使用Consumer小部件獲得對MyModel物件的引用;
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider(
create: (_) => MyModel(),
child: Scaffold(
appBar: AppBar(
title: Text('provider'),
),
body: Column(
children: <Widget>[
Builder(
builder: (context) {
// 獲取到provider提供出來的值
MyModel _model = Provider.of<MyModel>(context);
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightBlueAccent,
child: Text('當前是:${_model.counter}'));
},
),
Consumer<MyModel>(
// 獲取到provider提供出來的值
builder: (context, model, child) {
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightGreen,
child: Text(
'${model.counter}',
),
);
},
),
Consumer<MyModel>(
// 獲取到provider提供出來的值
builder: (context, model, child) {
return FlatButton(
color: Colors.tealAccent,
onPressed:model.incrementCounter,
child: Icon(Icons.add));
},
),
],
),
),
);
}
}
複製程式碼
點選FlatButton,model呼叫incrementCounter()函式,計數值加1。但是並不會重建UI,因為該Provider小部件不會監聽其提供的值的更改。
列印出計數值的變化
ChangeNotifierProvider
與最基礎的Provider小部件不同,ChangeNotifierProvider會監聽其提供出去的模型物件中的更改。當有值更改後,它將重建下方所有的Consumer和使用Provider.of(context)監聽並獲取提供值的地方。
程式碼中更改Provider為ChangeNotifierProvider。MyModel混入ChangeNotifier(繼承也一樣)。然後更改counter之後呼叫notifyListeners(),這樣ChangeNotifierProvider就會得到通知,並且Consumer和監聽的地方將重建其小部件。
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyModel(),
child: Scaffold(
appBar: AppBar(
title: Text('provider'),
),
body: Column(
children: <Widget>[
Builder(
builder: (context) {
MyModel _model = Provider.of<MyModel>(context);
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightBlueAccent,
child: Text('當前是:${_model.counter}'));
},
),
Consumer<MyModel>(
builder: (context, model, child) {
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightGreen,
child: Text(
'${model.counter}',
),
);
},
),
Consumer<MyModel>(
builder: (context, model, child) {
return FlatButton(
color: Colors.tealAccent,
onPressed: model.incrementCounter,
child: Icon(Icons.add));
},
),
],
),
),
);
}
}
class MyModel with ChangeNotifier{
// <--- MyModel
MyModel({this.counter = 0});
int counter = 0;
Future<void> incrementCounter() async {
await Future.delayed(Duration(seconds: 2));
counter++;
print(counter);
notifyListeners();
}
}
複製程式碼
每次點選,都會更改計數器的值,如果第一行的計數值是保留初始值,不更新呢?很簡單,把Provider.of的監聽器設定為false,這樣更改後就不會重新構建第一行的text:
MyModel _model = Provider.of<MyModel>(context,listen: false);
FutureProvider
FutureProvider基本上只是普通FutureBuilder的包裝。我們需要給它提供一些顯示在UI中的初始資料,還要為它設定要提供值的Future。在Future完成的時候,FutureProvider會通知Consumer重建自己的小部件。
在下面的程式碼中,使用了一個counter為0的MyModel向UI提供一些初始資料,並且新增了一個Future函式,可在3秒後返回一個counter為1的MyModel。 和基類Provider一樣,FutureProvider它不會監聽模型本身內的任何更改。在下面的程式碼中依舊通過按鈕點選事件使counter加1,但是對UI沒有影響。
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureProvider(
initialData: MyModel(counter: 0),
create: (context) => someAsyncFunctionToGetMyModel(),
child: Scaffold(
appBar: AppBar(
title: Text('provider'),
),
body: Column(
children: <Widget>[
Builder(
builder: (context) {
MyModel _model = Provider.of<MyModel>(context, listen: false);
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightBlueAccent,
child: Text('當前是:${_model.counter}'));
},
),
Consumer<MyModel>(
builder: (context, model, child) {
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightGreen,
child: Text(
'${model.counter}',
),
);
},
),
Consumer<MyModel>(
builder: (context, model, child) {
return FlatButton(
color: Colors.tealAccent,
onPressed: model.incrementCounter,
child: Icon(Icons.add));
},
),
],
),
),
);
}
Future<MyModel> someAsyncFunctionToGetMyModel() async {
// <--- async function
await Future.delayed(Duration(seconds: 3));
return MyModel(counter: 1);
}
}
class MyModel with ChangeNotifier {
// <--- MyModel
MyModel({this.counter = 0});
int counter = 0;
Future<void> incrementCounter() async {
await Future.delayed(Duration(seconds: 2));
counter++;
print(counter);
notifyListeners();
}
}
複製程式碼
FutureProvider通過設定的Future完成後會通知Consumer,重新build。但是,Future完成後,點選按鈕也不會更新UI。
FutureProvider適用於沒有重新整理和變更的頁面,和FutureBuilder一樣的作用。
StreamProvider
StreamProvider基本上是StreamBuilder的包裝,和上面的FutureProvider一樣。不同的是StreamProvider提供的是流,FutureProvider需要的一個Future。
StreamProvider也不會監聽model本身的變化。它僅監聽流中的新事件:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamProvider(
initialData: MyModel(counter: 0),
create: (context) => getStreamOfMyModel(),
child: Scaffold(
appBar: AppBar(
title: Text('provider'),
),
body: Column(
children: <Widget>[
Builder(
builder: (context) {
MyModel _model = Provider.of<MyModel>(context, listen: false);
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightBlueAccent,
child: Text('當前是:${_model.counter}'));
},
),
Consumer<MyModel>(
builder: (context, model, child) {
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightGreen,
child: Text(
'${model.counter}',
),
);
},
),
Consumer<MyModel>(
builder: (context, model, child) {
return FlatButton(
color: Colors.tealAccent,
onPressed: model.incrementCounter,
child: Icon(Icons.add));
},
),
],
),
),
);
}
Stream<MyModel> getStreamOfMyModel() {
return Stream<MyModel>.periodic(
Duration(seconds: 1), (x) => MyModel(counter: x)).take(10);
}
}
class MyModel with ChangeNotifier {
// <--- MyModel
MyModel({this.counter = 0});
int counter = 0;
Future<void> incrementCounter() async {
await Future.delayed(Duration(seconds: 2));
counter++;
print(counter);
notifyListeners();
}
}
複製程式碼
給StreamProvider設定了一個每隔1秒更新一次的stream,ui上的計數值也是每隔一秒改變一次。但是點選按鈕同樣不會重新整理ui。所以也可以認為是一個StreamBuilder。
ValueListenableProvider
ValueListenableProvider類似於ValueChange的封裝,它的作用和ChangeNotifierProvider一樣,在值改變的時候,會通知Consumer重新build,但是使用起來比ChangeNotifierProvider複雜,需要先用Provider提供MyModel給Consumer,然後把MyModel裡的ValueNotifier給ValueListenableProvider:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider<MyModel>(
create: (context) => MyModel(),
child: Consumer<MyModel>(
builder: (context, myModel, child) {
return ValueListenableProvider<int>.value(
value: myModel.counter,
child: Scaffold(
appBar: AppBar(
title: Text('provider'),
),
body: Column(
children: <Widget>[
Builder(
builder: (context) {
var count = Provider.of<int>(context);
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightBlueAccent,
child: Text('當前是:$count'));
},
),
Consumer<int>(
builder: (context, value, child) {
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightGreen,
child: Text(
'$value',
),
);
},
),
Consumer<MyModel>(
builder: (context, model, child) {
return FlatButton(
color: Colors.tealAccent,
onPressed: model.incrementCounter,
child: Icon(Icons.add));
},
),
],
),
),
);
}
),
);
}
}
class MyModel {
ValueNotifier<int> counter = ValueNotifier(0);
Future<void> incrementCounter() async {
await Future.delayed(Duration(seconds: 2));
print(counter.value++);
counter.value = counter.value;
}
}
複製程式碼
ListenableProvider
ListenableProvider和ChangeNotifierProvider一樣, 區別在於,如果Model是一個複雜模型ChangeNotifierProvider 會在你需要的時候,自動呼叫其 _disposer 方法,所以一般還是使用ChangeNotifierProvider即可。
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListenableProvider<MyModel>(
create: (context) => MyModel(),
child: Scaffold(
appBar: AppBar(
title: Text('provider'),
),
body: Column(
children: <Widget>[
Builder(
builder: (context) {
MyModel modol = Provider.of<MyModel>(context);
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightBlueAccent,
child: Text('當前是:${modol.counter}'));
},
),
Consumer<MyModel>(
builder: (context, model, child) {
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightGreen,
child: Text(
'${model.counter}',
),
);
},
),
Consumer<MyModel>(
builder: (context, model, child) {
return FlatButton(
color: Colors.tealAccent,
onPressed: model.incrementCounter,
child: Icon(Icons.add));
},
),
],
),
),
);
}
}
class MyModel with ChangeNotifier {
int counter = 0;
Future<void> incrementCounter() async {
await Future.delayed(Duration(seconds: 2));
counter++;
notifyListeners();
print(counter);
}
}
複製程式碼
MultiProvider
上面的示例都僅使用了一個Model物件。如果需要提供第二種型別的Model物件,可以巢狀Provider。但是,巢狀迷之縮排,可讀性低。這時候使用MultiProvider非常簡潔,
我們改下上面的計數器,一般首頁會有一個banner和列表。我們用上面的計數器模擬banner,下面的計數器模擬列表:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<BannerModel>(create: (context) => BannerModel()),
ChangeNotifierProvider<ListModel>(create: (context) => ListModel()),
],
child: Scaffold(
appBar: AppBar(
title: Text('provider'),
),
body: Column(
children: <Widget>[
Builder(
builder: (context) {
BannerModel modol = Provider.of<BannerModel>(context);
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightBlueAccent,
child: Text('當前Banner有幾個:${modol.counter}'));
},
),
Consumer<ListModel>(
builder: (context, model, child) {
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightGreen,
child: Text(
'當前Banner有幾個:${model.counter}',
),
);
},
),
Consumer<BannerModel>(
builder: (context, model, child) {
return FlatButton(
color: Colors.tealAccent,
onPressed: model.getBanner,
child: Text("獲取banner"));
},
),
Consumer<ListModel>(
builder: (context, model, child) {
return FlatButton(
color: Colors.tealAccent,
onPressed: model.getList,
child: Text("獲取列表"));
},
),
],
),
),
);
}
}
class BannerModel with ChangeNotifier {
int counter = 0;
Future<void> getBanner() async {
await Future.delayed(Duration(seconds: 2));
counter++;
notifyListeners();
print(counter);
}
}
class ListModel with ChangeNotifier {
int counter = 0;
Future<void> getList() async {
await Future.delayed(Duration(seconds: 2));
counter++;
notifyListeners();
print(counter);
}
}
複製程式碼
按下banner按鈕,就單獨獲取banner的數值,並更新banner的Consumer。列表的同理。
ProxyProvider
如果要提供兩個Model,但是其中一個Model取決於另一個Model,在這種情況下,可以使用ProxyProvider。A ProxyProvider從一個Provider獲取值,然後將其注入另一個Provider,
把上面的改下,比如的上傳圖片功能,需要先把圖片提交到圖片伺服器,然後再把連結傳送到後臺伺服器:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<PicModel>(create: (context) => PicModel()),
ProxyProvider<PicModel, SubmitModel>(
update: (context, myModel, anotherModel) => SubmitModel(myModel),
),
],
child: Scaffold(
appBar: AppBar(
title: Text('provider'),
),
body: Column(
children: <Widget>[
Builder(
builder: (context) {
PicModel modol = Provider.of<PicModel>(context);
return Container(
margin: const EdgeInsets.only(top: 20),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.lightBlueAccent,
child: Text('提交圖片:${modol.counter}'));
},
),
Consumer<PicModel>(
builder: (context, model, child) {
return FlatButton(
color: Colors.tealAccent,
onPressed: model.upLoadPic,
child: Text("提交圖片"));
},
),
Consumer<SubmitModel>(
builder: (context, model, child) {
return FlatButton(
color: Colors.tealAccent,
onPressed: model.subMit,
child: Text("提交"));
},
),
],
),
),
);
}
}
class PicModel with ChangeNotifier {
int counter = 0;
Future<void> upLoadPic() async {
await Future.delayed(Duration(seconds: 2));
counter++;
notifyListeners();
print(counter);
}
}
class SubmitModel {
PicModel _model;
SubmitModel(this._model);
Future<void> subMit() async {
await _model.upLoadPic();
}
}
複製程式碼
基於MVVM模式封裝Provider
相信大家都已經理解provider的流程,如下圖:
上面已經演示完了Provider的用法,在開發中,我們需要Model充當ViewModel,處理業務邏輯,但是每次都寫樣板程式碼的話也很麻煩,所以需要封裝下,易於使用。
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<LoginViewModel>(
create: (BuildContext context) {
return LoginViewModel(loginServive: LoginServive());
},
child: Scaffold(
appBar: AppBar(
title: Text('provider'),
),
body: Column(
children: <Widget>[
Consumer<LoginViewModel>(
builder: (context, model, child) {
return Text(model.info);
},
),
Consumer<LoginViewModel>(
builder: (context, model, child) {
return FlatButton(
color: Colors.tealAccent,
onPressed: () => model.login("pwd"),
child: Text("登入"));
},
),
],
),
),
);
}
}
/// viewModel
class LoginViewModel extends ChangeNotifier {
LoginServive _loginServive;
String info = '請登入';
LoginViewModel({@required LoginServive loginServive})
: _loginServive = loginServive;
Future<String> login(String pwd) async {
info = await _loginServive.login(pwd);
notifyListeners();
}
}
/// api
class LoginServive {
static const String Login_path = 'xxxxxx';
Future<String> login(String pwd) async {
return new Future.delayed(const Duration(seconds: 1), () => "登入成功");
}
}
複製程式碼
這種頁面寫法,基本每個頁面都要,下面我們一步一步開始封裝。
- 一般頁面載入的時候會顯示一個loading,然後載入成功展示資料,失敗就展示失敗頁面,所以列舉一個頁面狀態:
enum ViewState { Loading, Success,Failure }
複製程式碼
- ViewModel都會在頁面狀態屬性改變後更新ui,通常會呼叫notifyListeners,把這一步移到BaseModel中:
class BaseModel extends ChangeNotifier {
ViewState _state = ViewState.Loading;
ViewState get state => _state;
void setState(ViewState viewState) {
_state = viewState;
notifyListeners();
}
}
複製程式碼
- 我們知道ui裡需要ChangeNotifierProvider提供Model,並且用Consumer更新ui。因此我們也將其內建到BaseView中:
class BaseWidget<T extends ChangeNotifier> extends StatefulWidget {
final Widget Function(BuildContext context, T value, Widget child) builder;
final T model;
final Widget child;
BaseWidget({Key key, this.model, this.builder, this.child}) : super(key: key);
@override
State<StatefulWidget> createState() => _BaseWidgetState();
}
class _BaseWidgetState<T extends ChangeNotifier> extends State<BaseWidget<T>> {
T model;
@override
void initState() {
model = widget.model;
super.initState();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T>.value(
value: model,
child: Consumer<T>(
builder: widget.builder,
child: widget.child,
),
);
}
}
複製程式碼
- 有時候我們的頁面資料只是區域性更新,Consumer的child屬性就是模型更改時不需要重建的UI,所以我們將需要更新的ui放在builder裡,不需要更新的寫在child裡:
Consumer<LoginViewModel>(
// Pass the login header as a prebuilt-static child
child: LoginHeader(controller: _controller),
builder: (context, model, child) => Scaffold(
...
body: Column (
children: [
//不更新的部分
child,
...
]
)
複製程式碼
- 大多時候,我們已進入一個頁面,就要獲取資料,所以我們也把這個操作移入基類:
class BaseWidget<T extends ChangeNotifier> extends StatefulWidget {
final Function(T) onModelReady;
...
BaseWidget({
...
this.onModelReady,
});
...
}
...
@override
void initState() {
model = widget.model;
if (widget.onModelReady != null) {
widget.onModelReady(model);
}
super.initState();
}
複製程式碼
現在,我們用封裝的基類完成登入頁面:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BaseWidget<LoginViewModel>(
model: LoginViewModel(loginServive: LoginServive()),
builder: (context, model, child) => Scaffold(
appBar: AppBar(
title: Text('provider'),
),
body: Column(
children: <Widget>[
model.state == ViewState.Loading
? Center(
child: CircularProgressIndicator(),
)
: Text(model.info),
FlatButton(
color: Colors.tealAccent,
onPressed: () => model.login("pwd"),
child: Text("登入")),
],
),
),
);
}
}
/// viewModel
class LoginViewModel extends BaseModel {
LoginServive _loginServive;
String info = '請登入';
LoginViewModel({@required LoginServive loginServive})
: _loginServive = loginServive;
Future<String> login(String pwd) async {
setState(ViewState.Loading);
info = await _loginServive.login(pwd);
setState(ViewState.Success);
}
}
/// api
class LoginServive {
static const String Login_path = 'xxxxxx';
Future<String> login(String pwd) async {
return new Future.delayed(const Duration(seconds: 1), () => "登入成功");
}
}
enum ViewState { Loading, Success, Failure, None }
class BaseModel extends ChangeNotifier {
ViewState _state = ViewState.None;
ViewState get state => _state;
void setState(ViewState viewState) {
_state = viewState;
notifyListeners();
}
}
class BaseWidget<T extends ChangeNotifier> extends StatefulWidget {
final Widget Function(BuildContext context, T model, Widget child) builder;
final T model;
final Widget child;
final Function(T) onModelReady;
BaseWidget({
Key key,
this.builder,
this.model,
this.child,
this.onModelReady,
}) : super(key: key);
_BaseWidgetState<T> createState() => _BaseWidgetState<T>();
}
class _BaseWidgetState<T extends ChangeNotifier> extends State<BaseWidget<T>> {
T model;
@override
void initState() {
model = widget.model;
if (widget.onModelReady != null) {
widget.onModelReady(model);
}
super.initState();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T>(
create: (BuildContext context) => model,
child: Consumer<T>(
builder: widget.builder,
child: widget.child,
),
);
}
}
複製程式碼