原文在這裡。
雖然官方Flutter站點(狀態管理入門app)說Provider“非常容易理解”,我(作者)可不這麼認為。我想是因為Provider的種類有點多:
- Provider
- ListenableProvider
- ChangeNotifierProvider
- ValueListenableProvider
- StreamProvider
- FutureProvider
- MultiProvider
- ProxyProvider
- ChangeNotifierProxyProvider
- 更多
我只想用最簡單的方法管理我的app的狀態。為什麼有這麼多的選擇?我應該用哪一個呢?從哪裡開始呢?
本文的目的就是幫你理解主要的Provider型別是怎麼用的。我會給每一個Provide型別一個簡單的例子。幫助你理解,之後你就可以自己決定用哪個來管理你的app的狀態了。
準備
我的例子會使用這樣的佈局
也就是:
- 那個“Do something”按鈕代表改變app狀態的事件。
- 那個“Show something”文字Widget代表顯示app state的UI
- 左邊的綠色方框和右邊的藍色的方框代表了widget樹的不同部分。用來強調一個事件和與這個事件所更改的狀態相關的更新的UI。
這裡是程式碼:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('My App')),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: const EdgeInsets.all(20),
color: Colors.green[200],
child: RaisedButton(
child: Text('Do something'),
onPressed: () {},
),
),
Container(
padding: const EdgeInsets.all(35),
color: Colors.blue[200],
child: Text('Show something'),
),
],
),
),
);
}
}
這些例子需要你安裝Provider包
dependencies:
provider: ^4.0.1
而且已經import到了需要的地方:
import 'package:provider/provider.dart';
Provider
如你所想,Provider
是最基本的Provider型別。你可以用它來給widget樹的任何地方提供一個值(一般是data model物件)。然而,值發生變化的時候是不會幫你更新widget樹的。
假設你的app狀態在一個model裡:
class MyModel {
String someValue = 'Hello';
void doSomething() {
someValue = 'Goodbye';
print(someValue);
}
}
你可以用Provider包裝頂層Widget,給widget樹提供資料。使用Consume
widget來獲得這個資料的引用。
你可以在西面的程式碼找到Provider
和兩個Consume
widget:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider<MyModel>( // <--- Provider
create: (context) => MyModel(),
child: MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('My App')),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: const EdgeInsets.all(20),
color: Colors.green[200],
child: Consumer<MyModel>( // <--- Consumer
builder: (context, myModel, child) {
return RaisedButton(
child: Text('Do something'),
onPressed: (){
// We have access to the model.
myModel.doSomething();
},
);
},
)
),
Container(
padding: const EdgeInsets.all(35),
color: Colors.blue[200],
child: Consumer<MyModel>( // <--- Consumer
builder: (context, myModel, child) {
return Text(myModel.someValue);
},
),
),
],
),
),
),
);
}
}
class MyModel { // <--- MyModel
String someValue = 'Hello';
void doSomething() {
someValue = 'Goodbye';
print(someValue);
}
}
執行程式碼,你會看到這個結果:
NOTES:
- UI上顯示了mode物件的Hello。
- 點選“Do something”按鈕會引發一個導致model資料改變的事件。然而,即使model的資料改變了,UI也不會發生更改。因為
Provider
不會監聽model物件的資料變更。
ChangeNotifierProvider
與基本的Provider
widget不同,ChangeNotifierProvider
會監聽model物件的變化。當資料發生變更,它會重繪Consumer
的子widget。
在上面的程式碼裡,把Provider
換成ChangeNotifierProvider
。Model類也需要使用ChangeNotifier
mixin(or 擴充套件它)。這樣你可以使用notifyListeners()
方法。當你呼叫這個方法的時候,ChangeNotifierProvider
就會收到通知,Consumer
widget就會重繪它的子元件了。
下面是完整程式碼:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MyModel>( // <--- ChangeNotifierProvider
create: (context) => MyModel(),
child: MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('My App')),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: const EdgeInsets.all(20),
color: Colors.green[200],
child: Consumer<MyModel>( // <--- Consumer
builder: (context, myModel, child) {
return RaisedButton(
child: Text('Do something'),
onPressed: (){
myModel.doSomething();
},
);
},
)
),
Container(
padding: const EdgeInsets.all(35),
color: Colors.blue[200],
child: Consumer<MyModel>( // <--- Consumer
builder: (context, myModel, child) {
return Text(myModel.someValue);
},
),
),
],
),
),
),
);
}
}
class MyModel with ChangeNotifier { // <--- MyModel
String someValue = 'Hello';
void doSomething() {
someValue = 'Goodbye';
print(someValue);
notifyListeners();
}
}
現在當你點選了“Do something”按鈕,文字會從“Hello”變成“Goodbye”。
NOTE:
- 多數的app裡,model類都在各自的檔案裡。你需要在這些檔案裡import flutter/foundation.dart,這樣才能用
ChangeNotifier
。我不太喜歡這樣,因為這樣的話就表明你的業務邏輯裡包含了一個framework的依賴,而且這個framework還是一個和邏輯無關的“細節”。我們暫時先這樣。 - 當
notifyListeners()
方法後,Consumer
widget會重繪它的子樹的所有widget都會重繪。按鈕沒有必要更新,所以相對於Consumer
你可以用Provider.of
,並把listener設定為false。這樣model有變化,按鈕也不會更新了。這裡是修改了之後的按鈕widget。
class MyButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final myModel = Provider.of<MyModel>(context, listen: false);
return RaisedButton(
child: Text('Do something'),
onPressed: () {
myModel.doSomething();
},
);
}
}
下面的程式碼還是老樣子,使用Consumer
元件。
FutureProvider
FutureProvider
基本上是FutureBuilder
的一個封裝widget。你可以設定一些UI顯示的初始資料,然後可以給一個Future值。FutureProvider
會在Future完成的時候通知Consumer重繪它的widget子樹。
在下面的程式碼裡我給UI提供了一個空的model。我也加了一個方法,會在3秒鐘後返回一個新的model物件。這也是FutureProvider
在等待的。
和基本的Provider一樣,FutureProvider
不會監聽model本身的任何更改。在下面的程式碼裡會表明這一點。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureProvider<MyModel>( // <--- FutureProvider
initialData: MyModel(someValue: 'default value'),
create: (context) => someAsyncFunctionToGetMyModel(),
child: MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('My App')),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: const EdgeInsets.all(20),
color: Colors.green[200],
child: Consumer<MyModel>( // <--- Consumer
builder: (context, myModel, child) {
return RaisedButton(
child: Text('Do something'),
onPressed: (){
myModel.doSomething();
},
);
},
)
),
Container(
padding: const EdgeInsets.all(35),
color: Colors.blue[200],
child: Consumer<MyModel>( // <--- Consumer
builder: (context, myModel, child) {
return Text(myModel.someValue);
},
),
),
],
),
),
),
);
}
}
Future<MyModel> someAsyncFunctionToGetMyModel() async { // <--- async function
await Future.delayed(Duration(seconds: 3));
return MyModel(someValue: 'new data');
}
class MyModel { // <--- MyModel
MyModel({this.someValue});
String someValue = 'Hello';
Future<void> doSomething() async {
await Future.delayed(Duration(seconds: 2));
someValue = 'Goodbye';
print(someValue);
}
}
NOTE:
FutureProvider
會在Futuer<MyModel>
完成後通知Consumer
重繪。- 點選Hot restart重置app
- 注意,點選“Do something”按鈕不會更新UI,即使是在Future完成了之後。如果你想讓UI可以重繪,使用
ChangeNotifierProvider
。 FutureProvider
的使用情況一般是在處理網路或者文字讀取資料的時候。但是,也可以使用FutureBuilder
來達到同樣的目的。以我(作者)不成熟的看法,FutureProvider
沒有比FutureBuilder
更有用。如果我需要一個provider,我基本上會用ChangeNotifierProvider
,如果我不需要provider的話可能會用FutureProvider
。
StreamProvider
StreamProvider
是StreamBuilder
的一個封裝。你提供一個stream,然後Consumer
會在stream收到一個事件的時候更新它的widget子樹。這和上面的FutureProvider
非常相似。
你可以把stream傳送的值當做不可修改的。也就是說,StreamProvider
不會監聽model的變化。只會監聽stream裡的新事件。
程式碼如下:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamProvider<MyModel>( // <--- StreamProvider
initialData: MyModel(someValue: 'default value'),
create: (context) => getStreamOfMyModel(),
child: MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('My App')),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: const EdgeInsets.all(20),
color: Colors.green[200],
child: Consumer<MyModel>( // <--- Consumer
builder: (context, myModel, child) {
return RaisedButton(
child: Text('Do something'),
onPressed: (){
myModel.doSomething();
},
);
},
)
),
Container(
padding: const EdgeInsets.all(35),
color: Colors.blue[200],
child: Consumer<MyModel>( // <--- Consumer
builder: (context, myModel, child) {
return Text(myModel.someValue);
},
),
),
],
),
),
),
);
}
}
Stream<MyModel> getStreamOfMyModel() { // <--- Stream
return Stream<MyModel>.periodic(Duration(seconds: 1),
(x) => MyModel(someValue: '$x'))
.take(10);
}
class MyModel { // <--- MyModel
MyModel({this.someValue});
String someValue = 'Hello';
void doSomething() {
someValue = 'Goodbye';
print(someValue);
}
}
NOTE:
StreamProvider
在收到新的事件之後通知Consumer
重繪- 使用hot restart重置app
- 點選“Do something”不會重繪UI。如果你想要UI可以更新,那麼使用
ChangeNotifierProvider
。事實上,你可以在model里加一個stream,然後呼叫notifyListeners()
。這樣的話完全不需要StreamProvider
了。 - 你可以使用
StreamProvider
實現BLoC模式
ValueListenableProvider
你可以略過這一節,它和ChangeNotifierProvider
基本一樣,只是更加複雜一點,而且沒有什麼額外的好處。。。
如果你有一個這樣的ValueNotifier
:
class MyModel {
ValueNotifier<String> someValue = ValueNotifier('Hello');
void doSomething() {
someValue.value = 'Goodbye';
}
}
那麼你可以用ValueListenableProvider
來監聽這個值的變化。但是,如果你想在UI裡面呼叫model的方法,那麼你也需要在provider裡provide這個model物件。因此,你會在下面的程式碼裡看到一個Provider
提供了一個MyModel
物件給Consumer
,而這個provider同時也給了ValueListenableProvider
它需要的包含在MyModel
的ValueNotifier
屬性someValue
。也就是如果只是監聽一個model物件的屬性值只需要ValueListenableProvider
,但是你想在UI裡呼叫這個model的方法,那麼還要額外寫一個Provider
。
完整程式碼如下:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider<MyModel>(// <--- Provider
create: (context) => MyModel(),
child: Consumer<MyModel>( // <--- MyModel Consumer
builder: (context, myModel, child) {
return ValueListenableProvider<String>.value( // <--- ValueListenableProvider
value: myModel.someValue,
child: MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('My App')),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: const EdgeInsets.all(20),
color: Colors.green[200],
child: Consumer<MyModel>( // <--- Consumer
builder: (context, myModel, child) {
return RaisedButton(
child: Text('Do something'),
onPressed: (){
myModel.doSomething();
},
);
},
)
),
Container(
padding: const EdgeInsets.all(35),
color: Colors.blue[200],
child: Consumer<String>(// <--- String Consumer
builder: (context, myValue, child) {
return Text(myValue);
},
),
),
],
),
),
),
);
}),
);
}
}
class MyModel { // <--- MyModel
ValueNotifier<String> someValue = ValueNotifier('Hello'); // <--- ValueNotifier
void doSomething() {
someValue.value = 'Goodbye';
print(someValue.value);
}
}
不過作者本人對以上的看法後來發生了改變,他推薦大家看這篇文章。
ListenableProvider
如果你要定製一個provider,那麼就需要用到這個provider型別。不過文件還是提醒,你也許只是需要一個ChangeNotifierProvider
。所以,這個內容暫時忽略。之後我(作者)會更新這部分。
MultiProvider
到目前為止,我們的例子還是隻用過一個model物件。如果你需要另外的一個model物件,你就要巢狀provider了(就和在講解ValueListenableProvider
的時候一樣)。然而,巢狀只會造成混亂。一個更好的辦法就是使用MultiProvider
。
在下面的例子裡會使用ChangeNotifierProvider
提供兩種model物件。
下面是全部程式碼。有點長。只需要注意MultiProvider
,Consumer
和兩個model類。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider( // <--- MultiProvider
providers: [
ChangeNotifierProvider<MyModel>(create: (context) => MyModel()),
ChangeNotifierProvider<AnotherModel>(create: (context) => AnotherModel()),
],
child: MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('My App')),
body: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: const EdgeInsets.all(20),
color: Colors.green[200],
child: Consumer<MyModel>( // <--- MyModel Consumer
builder: (context, myModel, child) {
return RaisedButton(
child: Text('Do something'),
onPressed: (){
// We have access to the model.
myModel.doSomething();
},
);
},
)
),
Container(
padding: const EdgeInsets.all(35),
color: Colors.blue[200],
child: Consumer<MyModel>( // <--- MyModel Consumer
builder: (context, myModel, child) {
return Text(myModel.someValue);
},
),
),
],
),
// SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: const EdgeInsets.all(20),
color: Colors.red[200],
child: Consumer<AnotherModel>( // <--- AnotherModel Consumer
builder: (context, myModel, child) {
return RaisedButton(
child: Text('Do something'),
onPressed: (){
myModel.doSomething();
},
);
},
)
),
Container(
padding: const EdgeInsets.all(35),
color: Colors.yellow[200],
child: Consumer<AnotherModel>( // <--- AnotherModel Consumer
builder: (context, anotherModel, child) {
return Text('${anotherModel.someValue}');
},
),
),
],
),
],
),
),
),
);
}
}
class MyModel with ChangeNotifier { // <--- MyModel
String someValue = 'Hello';
void doSomething() {
someValue = 'Goodbye';
print(someValue);
notifyListeners();
}
}
class AnotherModel with ChangeNotifier { // <--- AnotherModel
int someValue = 0;
void doSomething() {
someValue = 5;
print(someValue);
notifyListeners();
}
}
NOTES:
- 點選第一個“Do something”按鈕,文字會從
Hello
變成Goodbye
。點選第二個“Do something”按鈕,文字會從0
變成5
。 - 這和單個的
ChangeNotifierProvider
沒有很大的不同。Consumer
通過不同的型別引數獲取到對應的model物件。
ProxyProvider
如果你有兩個model物件要提供,而且這兩個model物件之間還有依賴關係。在這個情況下你可以使用ProxyProvider
。一個ProxyProvider
從一個provider裡接受物件,然後把這個物件注入另外一個provider。
一開始你會對ProxyProvider
的使用方法有些困惑,這個例子可以幫你加深理解。
MultiProvider(
providers: [
ChangeNotifierProvider<MyModel>(
create: (context) => MyModel(),
),
ProxyProvider<MyModel, AnotherModel>(
update: (context, myModel, anotherModel) => AnotherModel(myModel),
),
],
基本的ProxyProvider
有兩個型別引數。第二個依賴於第一個。在ProxyProvider
的udpate
方法裡,第三個引數有anotherModel
存了上次的值,但是我們不在這裡使用。我們只是把myModel
當做引數傳入AnotherModel
的建構函式裡。
完整程式碼:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider( // <--- MultiProvider
providers: [
ChangeNotifierProvider<MyModel>( // <--- ChangeNotifierProvider
create: (context) => MyModel(),
),
ProxyProvider<MyModel, AnotherModel>( // <--- ProxyProvider
update: (context, myModel, anotherModel) => AnotherModel(myModel),
),
],
child: MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('My App')),
body: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: const EdgeInsets.all(20),
color: Colors.green[200],
child: Consumer<MyModel>( // <--- MyModel Consumer
builder: (context, myModel, child) {
return RaisedButton(
child: Text('Do something'),
onPressed: (){
myModel.doSomething('Goodbye');
},
);
},
)
),
Container(
padding: const EdgeInsets.all(35),
color: Colors.blue[200],
child: Consumer<MyModel>( // <--- MyModel Consumer
builder: (context, myModel, child) {
return Text(myModel.someValue);
},
),
),
],
),
Container(
padding: const EdgeInsets.all(20),
color: Colors.red[200],
child: Consumer<AnotherModel>( // <--- AnotherModel Consumer
builder: (context, anotherModel, child) {
return RaisedButton(
child: Text('Do something else'),
onPressed: (){
anotherModel.doSomethingElse();
},
);
},
)
),
],
),
),
),
);
}
}
class MyModel with ChangeNotifier { // <--- MyModel
String someValue = 'Hello';
void doSomething(String value) {
someValue = value;
print(someValue);
notifyListeners();
}
}
class AnotherModel { // <--- AnotherModel
MyModel _myModel;
AnotherModel(this._myModel);
void doSomethingElse() {
_myModel.doSomething('See you later');
print('doing something else');
}
}
NOTES:
- 開始文字顯示的是
Hello
。 - 當你點選“Do something”按鈕,
MyModel
物件的文字會變成Goodbye
。MyModel
通知了ChangeNotifierProvider
, UI也顯示了新的文字。 - 當你點選“Do something else”按鈕,
AnotherModel
接收MyModel
(ProxyProvider
注入),之後修改文字為"See you later"。因為MyModel
發生更改的時候通知了listener,所以UI發生了更新。如果AnotherModel
更改了它的值,UI是不會更新的。因為ProxyProvider
不會監聽更改。這時候可以使用ChangeNotifierProxyProvider
。 ProxyProvider
還是讓我有點迷糊。ChangeNotifierProxyProvider
本身就有更多的更多說明了,所以我不打算介紹更多了。- 我(作者)FilledStack用GetIt比用
ProxyProvider
實現依賴注入更好用。
Provider builder和value constructor
最後還有一件事需要解釋。
大部分的provider都有兩種建構函式。一個基本的接收一個create
方法,用來新建你的model物件。我們在上面的例子中基本都是這麼做的:
Provider<MyModel>(
create: (context) => MyModel(),
child: ...
)
可以看到MyModel
物件是在create
方法建立的。
如果你的物件已經建立好了,你只是想要提供一個它的引用。那麼你可以用命名構造方法value
:
final myModel = MyModel();
...
Provider<MyModel>.value(
value: myModel,
child: ...
)
這裡MyModel
物件提前就建好了,只提供了一個引用。如果你在initState
方法已經建好了物件,你就可以這麼做。
總結
基本上你可以忽略provider包裡的大多數類。只記住怎麼使用ChangeNotifierProvider
和Consumer
。如果你不想要更新UI,那麼就使用Provider
。使用Future和Stream的場景都可以把他們放在你的Model類裡面,然後通知ChangeNotifierProvider
。不需要FutureProvider
和StreamProvdier
。大多數的時候不需要MultiProvider
。如果有依賴注入的時候,也可以用GetIt包來實現,不需要用ProxyProvider
。這篇文章有更深入的介紹。
下一步: Riverpod
你也許聽說過Riverpod,這是一個構建在provider之上的狀態管理庫。作者也是同一個作者。如果你對provider的使用還有困惑,那麼riverpod只會讓你更加困惑。但是,一旦熟悉了這個庫,那麼它會讓一切變得簡單。我(作者)建議你試一試。我寫了一個Riverpod的教程希望對你有幫助。