從零開始的Flutter之旅: StatelessWidgetgit
從零開始的Flutter之旅: StatefulWidgetgithub
從零開始的Flutter之旅: InheritedWidgetsegmentfault
在上篇文章中咱們介紹了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。app
既然已經知道了緣由,那麼咱們再來思考下解決方案。框架
若是你對InheritedWidget不熟悉,推薦閱讀 從零開始的Flutter之旅: InheritedWidget
咱們來總結一下,在Column外套一層Widget,並將Column進行緩存,而後外層的Widget結合InheritedWidget來提供共享count的數據源。一旦count更新將會調用外層Widget的setState,而且從新build,但咱們使用的是Column緩存,同時CountText經過依賴的方式引用了共享的count數據源,從而會同步build更新。而RaisedButton使用的是未依賴的共享count數據源,因此並不會從新build。這樣就保證了只刷新CountText。less
這種方式統必定義爲Provider,其實Flutter內部已經有Provider的完整實現,不過咱們爲了學習這種解決方法的思想,本身來實現一個簡易版的Provider。以後再去看Flutter的Provider將會更加簡單。
方案已經有了,下面咱們直接來看具體實現細節。
實現一個本身的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; } }
爲了監聽共享數據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來通知訂閱的監聽者。
有了上面的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補給站】
或者掃描下方二維碼,與我創建有效的溝通,同時更快更準的收到個人更新推送。