前言
使用一種語言編寫各種應用的時候,橫亙在開發者面前的第一個問題就是如何進行狀態管理。在前端領域,我們習慣使用框架或者各種輔助庫來進行狀態管理。例如,開發者經常使用react自帶的context,或者mobx/redux等工具來管理元件間狀態。在大熱的跨端框架flutter中,筆者將對社群中使用廣泛的provider框架進行介紹。
準備工作
安裝與引入
provider pub連結
官方文件宣稱(本文基於4.0版本),provider是一個依賴注入和狀態管理的混合工具,通過元件來構建元件。
provider有以下三個特點:
- 可維護性,provider強制使用單向資料流
- 易測性/可組合性,provider可以很方便地模擬或者複寫資料
- 魯棒性,provider會在合適的時候更新元件或者模型的狀態,降低錯誤率
在pubspec.yaml檔案中加入如下內容:
dependencies:
provider: ^4.0.0
複製程式碼
然後執行命令flutter pub get
,安裝到本地。
使用時只需在檔案頭部加上如下內容:
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的構造方法可以巢狀使用
Provider<Something>(
create: (_) => Something(),
child: Provider<SomethingElse>(
create: (_) => SomethingElse(),
child: Provider<AnotherThing>(
create: (_) => AnotherThing(),
child: someWidget,
),
),
),
複製程式碼
上述程式碼看起來過於繁瑣,走入了巢狀地獄,好在provider給了更加優雅的實現
MultiProvider(
providers: [
Provider<Something>(create: (_) => Something()),
Provider<SomethingElse>(create: (_) => SomethingElse()),
Provider<AnotherThing>(create: (_) => AnotherThing()),
],
child: someWidget,
)
複製程式碼
代理provider(ProxyProvider)
在3.0版本之後,有一種新的代理provider可供使用,ProxyProvider
能夠將不同provider中的多個值整合成一個物件,並將其傳送給外層provider,當所依賴的多個provider中的任意一個發生變化時,這個新的物件都會更新。下面的例子使用ProxyProvider
來構建了一個依賴其他provider提供的計數器的例子
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來應對具體的需求
Provider
最基礎的provider,它會獲取一個值並將它暴露出來ListenableProvider
用來暴露可監聽的物件,該provider將會監聽物件的改變以便及時更新元件狀態ChangeNotifierProvider
ListerableProvider依託於ChangeNotifier的一個實現,它將會在需要的時候自動呼叫ChangeNotifier.dispose
方法ValueListenableProvider
監聽一個可被監聽的值,並且只暴露ValueListenable.value
方法StreamProvider
監聽一個流,並且暴露出其最近傳送的值FutureProvider
接受一個Future
作為引數,在這個Future
完成的時候更新依賴
專案實戰
接下來筆者將以自己專案來舉例provider的用法
首先定義一個基類,完成一些UI更新等通用工作
import 'package:provider/provider.dart';
class ProfileChangeNotifier extends ChangeNotifier {
Profile get _profile => Global.profile;
@override
void notifyListeners() {
Global.saveProfile(); //儲存Profile變更
super.notifyListeners();
}
}
複製程式碼
之後定義自己的資料類
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
方法劫持對資料的獲取和修改,在有相關改動發生時通知元件樹同步狀態。
在主檔案中,使用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
即可,但是這樣的寫法看上去不夠精簡,每次呼叫時都需要寫很長的一段開頭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));
}
}
複製程式碼
專案完整程式碼詳見本人倉庫
其他相關細節和常見問題(來自官方文件)
- 為什麼在
initState
中獲取Provider會報錯?
不要在只會呼叫一次的元件生命週期中呼叫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
的過程中,如果更新變數的值就會報出異常?
這個很有可能因為你在改變某個子元件的ChangeNotifier
時,整個渲染樹還處在建立過程中。
比較典型的使用場景是notifier中存在http請求
initState() {
super.initState();
Provider.of<Foo>(context).fetchSomething();
}
複製程式碼
這是不允許的,因為元件的更新是即時生效的。
換句話來說如果某些元件在非同步過程之前構建,某些元件在非同步過程之後構建,這很有可能觸發你應用中的UI表現不一致,這是不允許的。
為了解決這個問題,需要把你的非同步過程放在能夠等效的影響元件樹的地方
- 直接在你provider模型的建構函式中進行非同步過程
class MyNotifier with ChangeNotifier {
MyNotifier() {
_fetchSomething();
}
Future<void> _fetchSomething() async {}
}
複製程式碼
- 或者直接新增非同步行為
initState() {
super.initState();
Future.microtask(() =>
Provider.of<Foo>(context).fetchSomething(someValue);
);
}
複製程式碼
- 為了同步複雜的狀態,我必須使用
ChangeNotifier
嗎?
並不是,你可以使用一個物件來表示你的狀態,例如把Provider.value()
和StatefulWidget
結合起來使用,達到即重新整理狀態又同步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,
),
);
}
}
複製程式碼
當需要讀取狀態時:
return Text(Provider.of<int>(context).toString());
複製程式碼
當需要改變狀態時:
return FloatingActionButton(
onPressed: Provider.of<ExampleState>(context).increment,
child: Icon(Icons.plus_one),
);
複製程式碼
- 我可以封裝我自己的Provider麼?
可以,provider
暴露了許多細節api以便使用者封裝自己的provider,它們包括:SingleChildCloneableWidget
、InheritedProvider
、DelegateWidget
、BuilderDelegate
、ValueDelegate
等 - 我的元件重建得過於頻繁,這是為什麼?
可以使用Provider.of
來替代Consumer/Selector
.
可以使用可選的child
引數來保證元件樹只會重建某個特定的部分
Foo(
child: Consumer<A>(
builder: (_, a, child) {
return Bar(a: a, child: child);
},
child: Baz(),
),
)
複製程式碼
在以上例子中,當A
改變時,只有Bar
會重新渲染,Foo
和Baz
並不會進行不必要的重建。
為了更精細地控制,我們還可以使用Selector
來忽略某些不會影響元件數的改變。
Selector<List, int>(
selector: (_, list) => list.length,
builder: (_, length, __) {
return Text('$length');
}
);
複製程式碼
在這個例子中,元件只會在list的長度發生改變時才會重新渲染,其內部元素改變時並不會觸發重繪。
- 我可以使用兩個不同的provider來獲取同一個型別的值嗎?
不可以,哪怕你給多個provider定義了同一個型別,元件也只能獲取距離其最近的一個父元件中的provider的值.