Provider 做爲 Google 欽定的狀態管理框架,以其簡單易上手的特色,成爲大部分中小 App 的首選。Provider 的使用很是簡單,官方文檔也不長,基本上半個小時就能上手操做。但要用好 Provider 卻不簡單,這可關係到 App 的運行效率和流暢度。 下面我就總結了一些 Provider 在使用過程當中須要注意的 Tips,幫助你榨乾 Flutter 的最後一點性能!算法
⚠️ 提示:本文不是 Provider 入門教程,須要你對 Provider 有一個基本對了解。初學者建議跳轉到問末首先閱讀官方文檔 & 實例教學。bash
毫無疑問 Flutter 連帶整個第三方插件社區都在高密度的迭代,Provider 做爲一個發佈才1年多的庫現在已經迭代到 4.0 了。每一次更新不只僅是 Bug 的修復,還有大量功能的提高和性能的優化。好比 3.1 推出的 Selector,以及後期加入的針對性能的提示等。網絡
全部的 Provider 都有兩個構造方法,分別爲默認構造方法和便利構造方法。不少人簡單地認爲便利構造方法只是一種更加簡便的構造方法,它們接收的參數是同樣的。其實不對。 咱們以 ChangeNotifierProvider 爲例:框架
// ✅ 默認構造方法
ChangeNotifierProvider(
create: (_) => MyModel(),
child: ...
)
複製代碼
// ❌ 默認構造方法
MyModel myModel;
ChangeNotifierProvider(
create: (_) => myModel,
child: ...
)
複製代碼
// ✅ 便利構造方法
MyModel myModel;
ChangeNotifierProvider.value(
value: myModel,
child: ...
)
複製代碼
// ❌ 便利構造方法
ChangeNotifierProvider.value(
value: MyModel(),
child: ...
)
複製代碼
簡單的說就是,若是你須要初始化一個新的 Value ,就使用默認構造方法,經過 create 方法的返回值傳遞。而若是你已經有了這個 Value 的實例,則使用便利構造方法,直接賦值給 value 參數。具體的緣由能夠參考這個解答。less
因爲引入了 Provider 進行統一的狀態管理,所以大部分 Widget 再也不須要繼承自 StatefulWidget 來更新數據了。StatelessWidget 的維護成本比 StatefulWidget 要低,構建效率更高。同時更少的代碼量會讓咱們更容易地控制重建範圍,提升渲染效率。異步
固然,對於部分須要依附於 Widget 生命週期的邏輯(好比首次進入頁面進行 HTTP 請求),仍是得繼續使用 StatefulWidget 。async
Provider 取值有兩種方式,一種是 Provider.of(context) ,直接返回 Value。ide
因爲它是一個方法,沒法直接在 Widget 樹中調用,通常咱們放在 build 方法中,return 方法以前。工具
Widget build(BuildContext context) {
final text = Provider.of<String>(context);
return Container(child: Text(text));
}
複製代碼
可是,因爲 Provider 會監聽 Value 的變化而更新整個 context 上下文,所以若是 build 方法返回的 Widget 過大過於複雜的話,刷新的成本是很是高的。那麼咱們該如何進一步控制 Widget 的更新範圍呢?oop
一個辦法是將真正須要更新的 Widget 封裝成一個獨立的 Widget,將取值方法放到該 Widget 內部。
Widget build(BuildContext context) {
return Container(child: MyText());
}
class MyText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final text = Provider.of<String>(context);
return Text(text);
}
}
複製代碼
另外一個相對好一點的辦法是使用 Builder 方法創造一個範圍更小的 context。
Widget build(BuildContext context) {
return Container(child: Builder(builder: (context) {
final text = Provider.of<String>(context);
return Text(text);
}));
}
複製代碼
這兩種方法都可以在刷新 Widget 時跳過 Container 直接重建 Text 。不管哪一種方法,其根本目的就是縮小 Provider.of(context) 中 context 的範圍,減小 Widget 重建數量。但這兩個方法都太過繁瑣。
Consumer 是 Provier 的另外一種取值方式,不一樣的是它是一個 Widget ,可以方便的嵌入到 Widget 樹中調用,相似於上面的 Builder 方案。
Widget build(BuildContext context) {
return Container(child: Consumer<String>(
builder: (context, text, child) => Text(text),
));
}
複製代碼
Consumer 能夠直接拿到 context 連帶 Value 一併傳做爲參數傳遞給 builder ,使用無疑更加方便和直觀,大大下降了開發人員對於控制刷新範圍的工做成本。
Container 的 builder 方法有一個 child 屬性,咱們能夠將 Container 層級下不受 Value 影響的 Widget 寫到 child 中,這樣當 Value 更新時不會從新構建 child 中的 Widget ,進一步提升效率。
Widget build(BuildContext context) { return Container(child: Consumer<String>( builder: (context, text, child) => Row( children: <Widget>[ Text(text), child ], ), child: Text("不變的內容"), )); } 複製代碼
上面代碼中將不受 text 控制的 Text 放入 child 中並帶入 builder 方法,這樣當 text 改變時不會從新構建 child 中的 Text。
Selector 是 3.1 推出的功能,目的是更近一步的控制 Widget 的更新範圍,將監聽刷新的範圍控制到最小。 實際項目中咱們每每會根據業務場景或者頁面元素來設計 Provider 的 Value,此時的 Value 其實就是 ViewModel。大量的數據都放入 Value 的後果就是,只要一個值的改動,就會觸發整個 ViewModel 的 notifyListeners ,進而引起整個 ViewModel 關聯 Widget 的刷新。
所以,咱們須要一個能力,在執行刷新以前給咱們一次機會,判斷是否須要刷新,來避免不須要的刷新。這個能力,就是由 Selector 來實現的。
Selector<ViewModel, String>(
selector: (context, viewModel) => viewModel.title,
shouldRebuild: (pre, next) => pre != next,
builder: (context, title, child) => Text(name)
);
複製代碼
Selector 有兩個範型參數,分別是 Provider 的 Value 類型以及 Value 中具體用到的參數類型。它有三個參數:
有了 Selector ,咱們就能夠避免 ViewModel 中一人改動全家更新的尷尬了。但 Selector 的使用場景遠遠不限於 ViewModel 這種重 Value ,即使是用在單一數據上,Selector 也能盡最大限度榨乾性能。
好比一個數據列表 List ,若是修改其中一項數據,咱們每每會更新整個 ListView 中的 ListTile 。
return ListView.builder(itemBuilder: (context, index) {
final foo = Provider.of<ViewModel>(context).foos[index]
return ListTile(title: Text(foo.didSelected),);
});
複製代碼
若是經過 Performance 或者 Log 咱們會發現,只修改 foos 中的某一個 foo 的 didSelected 屬性,會將全部的 ListTile 都從新構建一遍。這無疑是沒有必要的。
return ListView.builder(itemBuilder: (context, index) {
return Selector< ViewModel, Foo>(
selector: (context, viewModel) => viewModel.foos[index],
shouldRebuild: (pre, next) => pre != next, // 此行能夠省略
builder: (context, foo, child) {
return ListTile(
title: Text(foo.didSelected),
);
},
);
});
複製代碼
經過 Selector 不只能在構建 Widget 的過程當中方便的獲取 Value ,還能在構建子 Widget 以前留給咱們一個額外的機會讓咱們決定是否須要從新構建子 Widget 。這樣,ListView 每次就只會重構被修改的那個 ListTile 了。
前面的 Consumer 彷佛能夠替代 Provider.of 的全部場景,那咱們還須要 Provider.of 嗎? 咱們經常有這樣的需求,就是隻須要取得上層 Provider 的 Value,不須要監聽並刷新數據,好比調用 Value 的方法。
Button(
onPressed: () =>
Provider.of<ViewModel>(context).run(),
)
複製代碼
上面這樣的寫法會報錯,由於 onPressed 方法只須要拿到 ViewModel 來調用 run 方法,它的內部不關心 ViewModel 是否有變化需不須要刷新。而 Provider.of 默認會監聽 ViewModel 的改變並影響運行效率。 其實 Provider.of(context) 方法有一個隱藏屬性 listen ,對於這種不關心 Value 是否變化只須要取值的狀況,只須要將 listen 設置爲 false(默認爲 true ),Provider.of 返回的 Value 就不會觸發監聽刷新啦。
Button(
onPressed: () =>
Provider.of<ViewModel>(context, listen: false).run(),
)
複製代碼
前面提到了,有些邏輯必須依賴 Widget 的生命週期,好比在進入頁面時訪問 Provider 。所以不少人會將邏輯放到 StatefulWidget 的 initState 或 didChangeDependencies 中。
initState() {
super.initState();
print(Provider.of<Foo>(context).value);
}
複製代碼
可是這麼作是有矛盾的。既然將 load 方法放到了 initState 回調中,就意味着你但願該方法在 Widget 生命週期內只走一次,也就就意味着此處的 Value 並不關心值會不會改變。
所以,若是你只是想要拿到 Value 而不須要監聽,直接使用上面的 listen 參數關閉監聽便可。
initState() {
super.initState();
print(Provider.of<Foo>(context, listen: false).value);
}
複製代碼
而若是你須要持續監聽 Value 並做出反應,則不該該將邏輯放入 initState 中,didChangeDependencies 更適合這樣的邏輯。可是因爲 didChangeDependencies 會頻繁調用屢次,獲取 Value 以後須要判斷一下 Value 是否有改變,避免 didChangeDependencies 方法死循環。
Value value;
didChangeDependencies() {
super.didChangeDependencies();
final value = Provider.of<Foo>(context).value;
if (value != this.value) {
this.value = value;
print(value);
}
}
複製代碼
可是!
以上方案只使用於訪問 Value ,若是須要修改 Value 並觸發更新(例如訪問網絡),則會報錯。由於 initState didChangeDependencies 中是不能觸發狀態更新的(包括調用 setState ),這樣可能會致使 Widgets 在上次構建還沒完成以前狀態就又被更新,最終致使狀態不統一。
所以,官方的建議是,若是 Provider Value 的方法不依賴外部參數,直接在 Value 初始化的時候執行方法。
class MyApi with ChangeNotifier {
MyApi() {
load();
}
Future<void> load() async {}
}
複製代碼
若是 Provider Value 的方法必須依賴 Widgets 提供的外部參數,能夠用 Future.microtask 將調用過程包在一個異步方法中。異步方法因爲 event loop 的緣故會推遲到下一個週期運行,避開了衝突。
initState() {
super.initState();
Future.microtask(() =>
Provider.of<MyApi>(context).load(page: page);
);
}
複製代碼
及時釋放再也不使用的資源是優化的重點。Provider 提供了兩套方案方便咱們及時釋放資源。
Provider(
create:(_) => Model(),
dispose:(context, value) {
// 釋放資源
}
)
複製代碼
class Model with ChangeNotifier {
@override
void dispose() {
// 釋放資源
super.dispose();
}
}
複製代碼
其實,這偏偏也是 ChangeNotifierProvider 和 ListenableProvider 的最大區別。ChangeNotifierProvider 繼承自 ListenableProvider ,只不過 ChangeNotifierProvider 對 Value 的類型要求更高,必須實現 ChangeNotifier ,而 dispose 是 ChangeNotifier 的一個方法。
除此以外,咱們還應該避免將全部 Provider 狀態都放置到頂層。雖然取用起來比較方便,但全局的 Provider 資源都沒法釋放,對性能的影響會愈來愈大。咱們應該在構建新頁面和新功能的時候就理清業務,讓 Provider 只覆蓋它所負責的範圍,並在退出該功能頁面後及時釋放資源。
最簡單最無腦的方式就是在 Widget 之間插入 Log 來觀察 Widget 的刷新範圍,一旦發現刷新範圍過大,和實際邏輯不符就應該嘗試查找優化點。這種排查方式雖然相對粗曠,但對於還沒有怎麼優化的項目而言效果顯著。
對於已經作過初步優化的項目而言,若是還想近一步榨乾 Flutter 的性能,就只能經過跑 Performance 搭配工具來分析出性能瓶頸。
其實上面全部的 Tips ,背後其實都在作一件事情:減小 Widget 的重建。
雖然咱們知道 Flutter 在內部作了大量高效的算法和策略來避免無效的重建和渲染,但再高效的算法也是有成本的,更況且算法對咱們來講是一個黑盒子,咱們沒法保證它能一直有效,所以咱們須要在源頭就掐斷無用的 Widget 重建。
最後是濃縮版的建議: