往期回顧
從零開始的Flutter之旅: StatelessWidget
從零開始的Flutter之旅: StatefulWidget
從零開始的Flutter之旅: InheritedWidget
在上篇文章中我們介紹了InheritedWidget,並在最後引發出一個問題。
雖然InheritedWidget可以提供共享資料,並且通過getElementForInheritedWidgetOfExactType來解除didChangeDependencies的呼叫,但還是沒有避免CountWidget的重新build,並沒有將build最小化。
我們今天就來解決如何避免不必要的build構建,將build縮小到最小的CountText。
分析
首先我們來分析下為什麼會導致父widget的重新build。
class CountWidget extends StatefulWidget {
@override
_CountState createState() {
return _CountState();
}
}
class _CountState extends State<CountWidget> {
int count = 0;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Count App',
theme: new ThemeData(primarySwatch: Colors.blue),
home: Scaffold(
appBar: AppBar(
title: Text("Count"),
),
body: Center(
child: CountInheritedWidget(
count: count,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CountText(),
RaisedButton(
onPressed: () => setState(() => count++),
child: Text("Increment"),
)
],
),
),
),
),
);
}
}
複製程式碼
為了方便分析,我把之前的程式碼提到這裡來。
我們來看,在點選RaisedButton的時候,我們會通過setState將count進行更新。而此時的setState方法的提供者是_CountState,即CountWidget。而state的改變會導致build的重新構建,導致的效果是CountWidget的build被重新呼叫,繼而它的子widget也相繼被重新build。
既然已經知道了原因,那麼我們再來思考下解決方案。
- 最簡單的,我們縮小setState提供者的範圍。現在是CountWidget,我們將其縮小到Column。
- 雖然已經縮小到了Column,但還是無法避免自身的build與其CountText之外的子Widget(RaisedButton)的重新build。如果我們將Column全部快取下來呢?我們在Column外層套一個Widget,並將其進行快取,一旦外層的Widget重新build,我們都使用Column的快取,這樣不就避免了Column的重新build。不過使用快取以後會有個問題,既然是快取,Center裡面的CountText也將不會改變。為了解決這個問題,我們就要使用上篇文章中的InheritedWidget。將整個Column放到InheritedWidget中,雖然Column是快取,但是CountText中引用了InheritedWidget中的count資料,一旦count發生改變,將會通知其進行重新build。這樣就保證了只重新整理CountText。
如果你對InheritedWidget不熟悉,推薦閱讀從零開始的Flutter之旅: InheritedWidget
我們來總結一下,在Column外套一層Widget,並將Column進行快取,然後外層的Widget結合InheritedWidget來提供共享count的資料來源。一旦count更新將會呼叫外層Widget的setState,並且重新build,但我們使用的是Column快取,同時CountText通過依賴的方式引用了共享的count資料來源,從而會同步build更新。而RaisedButton使用的是未依賴的共享count資料來源,所以並不會重新build。這樣就保證了只重新整理CountText。
這種方式統一定義為Provider,其實Flutter內部已經有Provider的完整實現,不過我們為了學習這種解決方法的思想,自己來實現一個簡易版的Provider。之後再去看Flutter的Provider將會更加簡單。
方案已經有了,下面我們直接來看具體實現細節。
實現
- 定義共享資料的ProviderInheritedWidget
- 定義監聽重新整理的NotifyModel
- 提供快取Widget的ModelProviderWidget
- 組裝替換原有實現方案
ProviderInheritedWidget
實現一個自己的InheritedWidget,主要用來提供共享資料來源,並接受快取的child。
class ProviderInheritedWidget<T> extends InheritedWidget {
final T data;
final Widget child;
ProviderInheritedWidget({@required this.data, this.child})
: super(child: child);
@override
bool updateShouldNotify(ProviderInheritedWidget oldWidget) {
// true -> 通知樹中依賴改共享資料的子widget
return true;
}
}
複製程式碼
NotifyModel
為了監聽共享資料count的變化,我們通過觀察者訂閱模式來實現。
class NotifyModel implements Listenable {
List _listeners = [];
@override
void addListener(listener) {
_listeners.add(listener);
}
@override
void removeListener(listener) {
_listeners.remove(listener);
}
void notifyDataSetChanged() {
_listeners.forEach((item) => item());
}
}
複製程式碼
Listenable提供一個簡單的監聽介面,通過add與remove來增加與移除監聽,然後提供一個notify方法來進行通知監聽者。
最後我們通過繼承NotifyModel來使count具有可監聽能力
class CountModel extends NotifyModel {
int count = 0;
CountModel({this.count});
void increment() {
count++;
notifyDataSetChanged();
}
}
複製程式碼
一旦count自增,就呼叫notifyDataSetChanged來通知訂閱的監聽者。
ModelProviderWidget
有了上面的Provider與Model,我們在提供一個外部Widget來統一管理它們,將它們結合起來。
class ModelProviderWidget<T extends NotifyModel> extends StatefulWidget {
final T data;
final Widget child;
// context 必須為當前widget的context
static T of<T>(BuildContext context, {bool listen = true}) {
return (listen ? context.dependOnInheritedWidgetOfExactType<ProviderInheritedWidget<T>>()
: (context.getElementForInheritedWidgetOfExactType<ProviderInheritedWidget<T>>()
.widget as ProviderInheritedWidget<T>)).data;
}
ModelProviderWidget({Key key, @required this.data, @required this.child})
: super(key: key);
@override
_ModelProviderState<T> createState() => _ModelProviderState<T>();
}
class _ModelProviderState<T extends NotifyModel>
extends State<ModelProviderWidget> {
void notify() {
setState(() {
print("notify");
});
}
@override
void initState() {
// 新增監聽
widget.data.addListener(notify);
super.initState();
}
@override
void dispose() {
// 移除監聽
widget.data.removeListener(notify);
super.dispose();
}
@override
void didUpdateWidget(ModelProviderWidget<T> oldWidget) {
// data 更新時移除老的data監聽
if (oldWidget.data != widget.data) {
oldWidget.data.removeListener(notify);
widget.data.addListener(notify);
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return ProviderInheritedWidget<T>(
data: widget.data,
child: widget.child,
);
}
}
複製程式碼
在這裡我們提供可監聽的data資料與需要快取的child,同時在state中對可監聽的data在合適的地方進行監聽訂閱與移除訂閱,並在收到data資料改變時呼叫notify進行setState操作,通知widget重新整理。
在build中引用了ProviderInheritedWidget,來實現對共享子widget的資料共享,同時在ModelProviderWidget中提供of方法來暴露ProviderInheritedWidget的統一獲取方式。
通過引數listen(預設true)來控制獲取共享資料的方式,來決定是否建立依賴關係,即共享資料改變時,引用共享資料的widget是否重新build。
這一幕是不是有點似曾相識,基本上都是上篇文章中提到的InheritedWidget使用的細節。
接下來就是最終的方案替換
組裝替換原有實現方案
我們通過ModelProviderWidget.of來獲取共享的資料,所以只要使用到了共享資料,將要呼叫該方法。為了避免不必要的重複書寫,我們將其單獨封裝到Consumer中,內部來實現對其的呼叫,並且將呼叫的結果暴露出來。
class Consumer<T> extends StatelessWidget {
final Widget Function(BuildContext context, T value) builder;
const Consumer({Key key, @required this.builder}) : super(key: key);
@override
Widget build(BuildContext context) {
print("Consumer build");
return builder(context, ModelProviderWidget.of<T>(context));
}
}
複製程式碼
一切準備就緒,我們再對之前的程式碼進行優化。
class CountWidget extends StatefulWidget {
@override
_CountState createState() {
return _CountState();
}
}
class _CountState extends State<CountWidget> {
@override
Widget build(BuildContext context) {
print("CountWidget build");
return MaterialApp(
title: 'Count App',
theme: new ThemeData(primarySwatch: Colors.blue),
home: Scaffold(
appBar: AppBar(
title: Text("Count"),
),
body: Center(
child: ModelProviderWidget<CountModel>(
data: CountModel(count: 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Consumer<CountModel>(
builder: (context, value) => Text("count: ${value.count}")),
Builder(
builder: (context) {
print("RaiseButton build");
return RaisedButton(
onPressed: () => ModelProviderWidget.of<CountModel>(
context,
listen: false)
.increment(),
child: Text("Increment"),
);
},
),
],
),
),
),
),
);
}
}
複製程式碼
我們將Column快取到ModelProviderWidget中,同時對CountModel資料進行共享;通過Consumer進行Text的封裝,引用共享資料CountModel中的count。
對於RaisedButton,因為它只是提供點選,並且觸發count的自增操作、沒有發生ui上的任何變化。所以為了避免RaisedButton引用的共享資料進行自增時重新build,這裡將listen引數置為false。
最後我們執行上面的程式碼,我們點選Increment按鈕時,控制檯將會輸出如下日誌:
I/flutter ( 3141): notify
I/flutter ( 3141): Consumer build
複製程式碼
說明只有Consumer重新呼叫了build,即Text進行了重新整理。其它的widget都沒有變化。
這樣就解決了開篇提到的疑問,達到了widget重新整理的最小化。
以上是一個簡單地Provider-Consumer的使用。Flutter對這一塊有更完善的實現方案。但是經過我們這一輪分析,你再去看Flutter中Provider的原始碼將會更加簡單易懂。
如果你想了解Flutter中Provider的使用,你可以通過flutter_github來了解它的具體實戰使用技巧。
想要檢視Provider實戰技巧,需要將分支切換到
sample_provider
推薦專案
下面介紹一個完整的Flutter專案,對於新手來說是個不錯的入門。
flutter_github,這是一個基於Flutter的Github客戶端同時支援Android與IOS,支援賬戶密碼與認證登陸。使用dart語言進行開發,專案架構是基於Model/State/ViewModel的MSVM;使用Navigator進行頁面的跳轉;網路框架使用了dio。專案正在持續更新中,感興趣的可以關注一下。
當然如果你想了解Android原生,相信flutter_github的純Android版本AwesomeGithub是一個不錯的選擇。
如果你喜歡我的文章模式,或者對我接下來的文章感興趣,建議您關注我的微信公眾號:【Android補給站】
或者掃描下方二維碼,與我建立有效的溝通,同時更快更準的收到我的更新推送。