前言
Flutter的很多靈感來自於React,它的設計思想是資料與檢視分離,由資料對映渲染檢視。所以在Flutter中,它的Widget是immutable的,而它的動態部分全部放到了狀態(State)中。
假如你曾進行過react開發,也許你一下會想到Redux。flutter有類似redux的狀態管理的庫嗎?答案是肯定的,但是有關在flutter中使用redux的應用實踐我們會在之後的文章中進行介紹。
這個系列將會從這幾個狀態管理方案進行深入研究:
- Scoped_model
- redux
- BLoC
- 對比總結篇
今天要和大家分享的是第一篇,使用Scoped_model進行狀態管理。
為什麼需要狀態管理
在我們一開始構建應用的時候,也許很簡單。我們有一些狀態,直接把他們對映成檢視就可以了。這種簡單應用可能並不需要狀態管理。
但是隨著功能的增加,你的應用程式將會有幾十個甚至上百個狀態。這個時候你的應用應該會是這樣。
Wow,這是什麼鬼。我們很難再清楚的測試維護我們的狀態,因為它看上去實在是太複雜了!而且還會有多個頁面共享同一個狀態,例如當你進入一個文章點贊,退出到外部縮略展示的時候,外部也需要顯示點贊數,這時候就需要同步這兩個狀態。這時候,我們便迫切的需要一個架構來幫助我們理清這些關係,狀態管理框架應運而生。
什麼是Scoped_model
Scoped_model是一個dart第三方庫,提供了讓您能夠輕鬆地將資料模型從父Widget傳遞到它的後代的功能。此外,它還會在模型更新時重新渲染使用該模型的所有子項。
它直接來自於Google正在開發的新系統Fuchsia核心Widgets 中對Model類的簡單提取,作為獨立使用的獨立Flutter外掛釋出。
實現原理
Scoped model使用了觀察者模式,將資料模型放在父代,後代通過找到父代的model進行資料渲染,最後資料改變時將資料傳回,父代再通知所有用到了該model的子代去更新狀態。
而我們則需要將它們放在頂層入口MaterialApp之上,這樣就能進行全域性的狀態管理了。
這裡page3,page4代表使用到該狀態(model)的子頁面。Lets do it!
這裡我們以一個最簡單的CountApp舉例,詳細介紹Scoped_model的用法。該專案完整程式碼已放在github倉庫。
這是一個在不同頁面使用Scoped共享狀態資訊的app。這兩個頁面都依賴於一個數字,這個數字會隨著我們按下按鈕的次數而增加。
第一步:新增依賴
在pubspec中新增scoped_model的依賴。
- 實際新增請參考:pub.dartlang.org/packages/sc…
- 由於版本衝突新增失敗請參考:juejin.im/post/5b8958…
第二步:建立Model
在Scoped中,Model是一個只包含與狀態相關資訊的單位。我們應該把狀態資料與運算元據的方法抽象出來封裝到Model中。
import 'package:scoped_model/scoped_model.dart';
class CountModel extends Model{
int _count = 0;
get count => _count;
void increment(){
_count++;
notifyListeners();
}
}
複製程式碼
- 我們需要讓我們自定義的CountModel繼承至Model。
- 在狀態發生變化時(increment)通知所有用到了該model的子項更新狀態。(notifyListeners)
第三步:將Model放入頂層
//建立頂層狀態
CountModel countModel = CountModel();
@override
Widget build(BuildContext context) {
return ScopedModel<CountModel>(
model: countModel,
child: new MaterialApp(
home: TopScreen(),
),
);
}
複製程式碼
- 我們在頂層建立了一個CountModel的例項。
- ScopedModel<T extends Model>是一個StatelessWidget,它接收一個model,並提供給需要它的所有部件。
- 將ScopedModel<T extends Model>的model屬性繫結我們的CountModel物件。
第四步:在子頁面中獲取Model
我們可以從前面的演示圖片中看出,一共有兩個頁面,都使用了同一個model。 Scoped_model提供了兩種方式在子頁面中獲取model。我們先來介紹第一種,使用ScopedModelDescendant獲取model。
@override
Widget build(BuildContext context) {
return ScopedModelDescendant<CountModel>(
builder: (context,child,model){
return Scaffold(
body: Center(
child: Text(
model.count.toString(),
style: TextStyle(fontSize: 48.0),
),
),
);
},
);
}
複製程式碼
- ScopedModelDescendant<T extends Model>是一個Stateless Widget,它接收三個引數。
- builder是一個ScopedModelDescendantBuilder,它接收三個引數。 ,在builder中能夠通過model來獲取CountModel例項。
- rebuildOnChange屬效能夠控制當該狀態發生變化時,是否rebuild,作用等同於setState。也就是說我們呼叫改變狀態的一些方法時,不必再setState。
floatingActionButton: new FloatingActionButton(
onPressed: () => model.increment(),
tooltip: 'Increment',
child: new Icon(Icons.add),
)
複製程式碼
第二種獲取model的方式——使用ScopedModel.of
final countModel = ScopedModel.of<CountModel>(context);
countModel.increment();
複製程式碼
或者在Model中重寫of方法
class CountModel extends Model{
int _count = 0;
get count => _count;
void increment(){
_count++;
notifyListeners();
}
//重寫of方法
CountModel of(context) =>
ScopedModel.of<CountModel>(context);
}
複製程式碼
然後直接通過CountModel獲取model例項
final countModel2 = CountModel().of(context);
複製程式碼
這種方式似乎讓我們的程式碼有更好的可閱讀性。
【注意:】我們在使用第二種方式的時候,rebuildOnChange屬性預設為false,所以會導致無法重新整理(同步)狀態的情況發生,需要手動指定rebuildOnChange:true。這裡要非常感謝@榮毅coolboy同學的分享!
Q&A
這裡看上去似乎只新增了一個model,我應該如何新增多個model
要解決這個問題很簡單,使用Mixin!
class MainModel extends Model with AModel,BModel,CModel{}
複製程式碼
然後將MainModel放在頂層即可。 這裡有一個比較完整的使用ScopedModel管理狀態的應用,詳細用法可參考該專案。
Scoped是如何做到同步不同頁面中的狀態的
Model實現了Listenable介面,並重寫了void addListener(VoidCallback listener),removeListener(VoidCallback listener)方法,實現了觀察者模式。 每當我們呼叫notifyListeners()方法時,將會通知觀察者更新狀態。Scoped如何做到資料能夠互相共享的
在不同頁面間的資料傳遞使用了InheritedWidget。
侵入性
由於Model必須繼承至Model類,所以它就具有了侵入性。以後假如不用scoped進行狀態管理那麼必然會帶來需要更改多處程式碼的情況。這並不是我們希望看到的結果。
寫在最後
在flutter中,Scoped_model是一種非常簡單易上手,並能保持程式碼高可閱讀性的一種新的狀態管理方式,值得各位去嘗試一下!
本次所用到的程式碼已經上傳Github: github.com/Vadaski/Vad…
如果您對scoped還有任何疑問或者文章的建議,歡迎在下方評論區以及我的郵箱1652219550a@gmail.com與我聯絡,我會及時回覆!
下一章我們將探索Redux在Flutter中的實踐,敬請關注。