前言
??使用一種語言編寫各種應(yīng)用的時候,橫亙在開發(fā)者面前的第一個問題就是如何進(jìn)行狀態(tài)管理。在前端領(lǐng)域渔扎,我們習(xí)慣使用框架或者各種輔助庫來進(jìn)行狀態(tài)管理。例如信轿,開發(fā)者經(jīng)常使用react自帶的context,或者mobx/redux等工具來管理組件間狀態(tài)晃痴。在大熱的跨端框架flutter中,筆者將對社區(qū)中使用廣泛的provider框架進(jìn)行介紹财忽。
準(zhǔn)備工作
安裝與引入
provider pub鏈接
官方文檔宣稱(本文基于4.0版本)倘核,provider是一個依賴注入和狀態(tài)管理的混合工具,通過組件來構(gòu)建組件即彪。
provider有以下三個特點(diǎn):
- 可維護(hù)性紧唱,provider強(qiáng)制使用單向數(shù)據(jù)流
- 易測性/可組合性,provider可以很方便地模擬或者復(fù)寫數(shù)據(jù)
- 魯棒性,provider會在合適的時候更新組件或者模型的狀態(tài)漏益,降低錯誤率
在pubspec.yaml文件中加入如下內(nèi)容:
dependencies:
provider: ^4.0.0
然后執(zhí)行命令flutter pub get
,安裝到本地蛹锰。
使用時只需在文件頭部加上如下內(nèi)容:
import 'package:provider/provider.dart';
暴露一個值
如果我們想讓某個變量能夠被一個widget及其子widget所引用,我們需要將其暴露出來绰疤,典型寫法如下:
Provider(
create: (_) => new MyModel(),
child: ...
)
讀取一個值
如果要使用先前暴露的對象铜犬,可以這樣操作
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
MyModel yourValue = Provider.of<MyModel>(context)
return ...
}
}
暴露和使用多個值(MultiProvider)
Provider的構(gòu)造方法可以嵌套使用
Provider<Something>(
create: (_) => Something(),
child: Provider<SomethingElse>(
create: (_) => SomethingElse(),
child: Provider<AnotherThing>(
create: (_) => AnotherThing(),
child: someWidget,
),
),
),
上述代碼看起來過于繁瑣,走入了嵌套地獄峦睡,好在provider給了更加優(yōu)雅的實(shí)現(xiàn)
MultiProvider(
providers: [
Provider<Something>(create: (_) => Something()),
Provider<SomethingElse>(create: (_) => SomethingElse()),
Provider<AnotherThing>(create: (_) => AnotherThing()),
],
child: someWidget,
)
代理provider(ProxyProvider)
在3.0版本之后翎苫,有一種新的代理provider可供使用,ProxyProvider
能夠?qū)⒉煌琾rovider中的多個值整合成一個對象榨了,并將其發(fā)送給外層provider煎谍,當(dāng)所依賴的多個provider中的任意一個發(fā)生變化時,這個新的對象都會更新龙屉。下面的例子使用ProxyProvider
來構(gòu)建了一個依賴其他provider提供的計數(shù)器的例子
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
ProxyProvider<Counter, Translations>(
create: (_, counter, __) => Translations(counter.value),
),
],
child: Foo(),
);
}
class Translations {
const Translations(this._value);
final int _value;
String get title => 'You clicked $_value times';
}
各種provider
可以通過各種不同的provider來應(yīng)對具體的需求
-
Provider
最基礎(chǔ)的provider,它會獲取一個值并將它暴露出來 -
ListenableProvider
用來暴露可監(jiān)聽的對象呐粘,該provider將會監(jiān)聽對象的改變以便及時更新組件狀態(tài) -
ChangeNotifierProvider
ListerableProvider依托于ChangeNotifier的一個實(shí)現(xiàn),它將會在需要的時候自動調(diào)用ChangeNotifier.dispose
方法 -
ValueListenableProvider
監(jiān)聽一個可被監(jiān)聽的值转捕,并且只暴露ValueListenable.value
方法 -
StreamProvider
監(jiān)聽一個流作岖,并且暴露出其最近發(fā)送的值 -
FutureProvider
接受一個Future
作為參數(shù),在這個Future
完成的時候更新依賴
項(xiàng)目實(shí)戰(zhàn)
接下來筆者將以自己項(xiàng)目來舉例provider的用法
首先定義一個基類五芝,完成一些UI更新等通用工作
import 'package:provider/provider.dart';
class ProfileChangeNotifier extends ChangeNotifier {
Profile get _profile => Global.profile;
@override
void notifyListeners() {
Global.saveProfile(); //保存Profile變更
super.notifyListeners();
}
}
之后定義自己的數(shù)據(jù)類
class UserModle extends ProfileChangeNotifier {
String get user => _profile.user;
set user(String user) {
_profile.user = user;
notifyListeners();
}
bool get isLogin => _profile.isLogin;
set isLogin(bool value) {
_profile.isLogin = value;
notifyListeners();
}
String get avatar => _profile.avatar;
set avatar(String value) {
_profile.avatar = value;
notifyListeners();
}
這里通過set
和get
方法劫持對數(shù)據(jù)的獲取和修改痘儡,在有相關(guān)改動發(fā)生時通知組件樹同步狀態(tài)。
在主文件中枢步,使用provider
class MyApp extends StatelessWidget with CommonInterface {
MyApp({Key key, this.info}) : super(key: key);
final info;
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
UserModle newUserModel = new UserModle();
return MultiProvider(
providers: [
// 用戶信息
ListenableProvider<UserModle>.value(value: newUserModel),
],
child: ListenContainer(),
);
}
}
接下來沉删,在所有的子組件中,如果需要使用用戶的名字醉途,只需Provider.of<UserModle>(context).user
即可矾瑰,但是這樣的寫法看上去不夠精簡,每次調(diào)用時都需要寫很長的一段開頭Provider.of<xxx>(context).XXX
很是繁瑣隘擎,故而這里我們可以簡單封裝一個抽象類:
abstract class CommonInterface {
String cUser(BuildContext context) {
return Provider.of<UserModle>(context).user;
}
}
在子組件聲明時殴穴,使用with
,來簡化代碼
class MyApp extends StatelessWidget with CommonInterface {
......
}
在使用時只需cUser(context)
即可货葬。
class _FriendListState extends State<FriendList> with CommonInterface {
@override
Widget build(BuildContext context) {
return Text(cUser(context));
}
}
項(xiàng)目完整代碼詳見本人倉庫
其他相關(guān)細(xì)節(jié)和常見問題(來自官方文檔)
- 為什么在
initState
中獲取Provider會報錯?
不要在只會調(diào)用一次的組件生命周期中調(diào)用Provider,比如如下的使用方法是錯誤的
initState() {
super.initState();
print(Provider.of<Foo>(context).value);
}
要解決這個問題采幌,要么使用其他生命周期方法(didChangeDependencies/build)
didChangeDependencies() {
super.didChangeDependencies();
final value = Provider.of<Foo>(context).value;
if (value != this.value) {
this.value = value;
print(value);
}
}
或者指明你不在意這個值的更新,比如
initState() {
super.initState();
print(Provider.of<Foo>(context, listen: false).value);
}
- 我在使用
ChangeNotifier
的過程中震桶,如果更新變量的值就會報出異常?
這個很有可能因?yàn)槟阍诟淖兡硞€子組件的ChangeNotifier
時休傍,整個渲染樹還處在創(chuàng)建過程中。
比較典型的使用場景是notifier中存在http請求
initState() {
super.initState();
Provider.of<Foo>(context).fetchSomething();
}
這是不允許的尼夺,因?yàn)榻M件的更新是即時生效的尊残。
換句話來說如果某些組件在異步過程之前構(gòu)建,某些組件在異步過程之后構(gòu)建淤堵,這很有可能觸發(fā)你應(yīng)用中的UI表現(xiàn)不一致寝衫,這是不允許的。
為了解決這個問題拐邪,需要把你的異步過程放在能夠等效的影響組件樹的地方
- 直接在你provider模型的構(gòu)造函數(shù)中進(jìn)行異步過程
class MyNotifier with ChangeNotifier {
MyNotifier() {
_fetchSomething();
}
Future<void> _fetchSomething() async {}
}
- 或者直接添加異步行為
initState() {
super.initState();
Future.microtask(() =>
Provider.of<Foo>(context).fetchSomething(someValue);
);
}
- 為了同步復(fù)雜的狀態(tài)慰毅,我必須使用
ChangeNotifier
嗎?
并不是,你可以使用一個對象來表示你的狀態(tài)扎阶,例如把Provider.value()
和StatefulWidget
結(jié)合起來使用,達(dá)到即刷新狀態(tài)又同步UI的目的.
class Example extends StatefulWidget {
const Example({Key key, this.child}) : super(key: key);
final Widget child;
@override
ExampleState createState() => ExampleState();
}
class ExampleState extends State<Example> {
int _count;
void increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Provider.value(
value: _count,
child: Provider.value(
value: this,
child: widget.child,
),
);
}
}
當(dāng)需要讀取狀態(tài)時:
return Text(Provider.of<int>(context).toString());
當(dāng)需要改變狀態(tài)時:
return FloatingActionButton(
onPressed: Provider.of<ExampleState>(context).increment,
child: Icon(Icons.plus_one),
);
- 我可以封裝我自己的Provider么?
可以,provider
暴露了許多細(xì)節(jié)api以便使用者封裝自己的provider,它們包括:SingleChildCloneableWidget
牢裳、InheritedProvider
参咙、DelegateWidget
、BuilderDelegate
惰赋、ValueDelegate
等 - 我的組件重建得過于頻繁宰掉,這是為什么?
可以使用Provider.of
來替代Consumer/Selector
.
可以使用可選的child
參數(shù)來保證組件樹只會重建某個特定的部分
Foo(
child: Consumer<A>(
builder: (_, a, child) {
return Bar(a: a, child: child);
},
child: Baz(),
),
)
在以上例子中,當(dāng)A
改變時赁濒,只有Bar
會重新渲染轨奄,Foo
和Baz
并不會進(jìn)行不必要的重建。
為了更精細(xì)地控制拒炎,我們還可以使用Selector
來忽略某些不會影響組件數(shù)的改變挪拟。
Selector<List, int>(
selector: (_, list) => list.length,
builder: (_, length, __) {
return Text('$length');
}
);
在這個例子中,組件只會在list的長度發(fā)生改變時才會重新渲染击你,其內(nèi)部元素改變時并不會觸發(fā)重繪玉组。
- 我可以使用兩個不同的provider來獲取同一個類型的值嗎?
不可以,哪怕你給多個provider定義了同一個類型果漾,組件也只能獲取距離其最近的一個父組件中的provider的值.