前言
2019 Google I/O 大會,google就推出Provider,成為官方推薦的狀態管理方式之一,Flutter 狀態管理一直是個很熱門的話題,而且狀態管理的庫也是超級多,這確實是我們每一個做Flutter開發難以避免的一道坎,既然這麼重要,我們如何去理解它,如何使用它,如何做到更好呢?接下來讓我告訴你答案
囉嗦幾句
該文章已經經歷了一週的迭代,預計還要一週左右,要做一個全面的分析,當然要每個細節都要關注到,如果您覺得好,請不要吝嗇您的大拇指,順便點個贊哦,麼麼噠,如果有不對的地方提出來,一個地方一個紅包獎勵哦,愛你們。
主要內容
一張圖告訴你,我要講的主要內容。下面將圍繞這八個方面來講。七個理論,一個實踐。
- 狀態管理是什麼
- 為什麼需要狀態管理
- 狀態管理基本分類
- 狀態管理的底層邏輯
- 狀態管理的使用原則
- 使用成熟狀態管理庫的弊端
- 選擇狀態管理庫的原則
- Provider 深入分析(學以致用)
狀態管理是什麼
我們知道最基本的程式是什麼:
- 程式=演算法+資料結構
資料是程式的中心。資料結構和演算法兩個概念間的邏輯關係貫穿了整個程式世界,首先二者表現為不可分割的關係。其實Flutter不就是一個程式嗎,那我們面臨的最底層的問題還是演算法和資料結構,所以我們推匯出
- Flutter=演算法+資料結構
那狀態管理是什麼?我也用公式來表達一下,如下:
- Flutter狀態管理=演算法+資料結構+UI繫結
瞬間秒懂有沒有?來看一個程式碼例子:
class ThemeBloc {
final _themeStreamController = StreamController<AppTheme>();
get changeTheTheme => _themeStreamController.sink.add;
get darkThemeIsEnabled => _themeStreamController.stream;
dispose() {
_themeStreamController.close();
}
}
final bloc = ThemeBloc();
class AppTheme {
ThemeData themeData;
AppTheme(this.themeData);
}
/// 繫結到UI
StreamBuilder<AppTheme>(
initialData: AppTheme.LIGHT_THEME,
stream: bloc.darkThemeIsEnabled,
builder: (context, AsyncSnapshot<AppTheme> snapshot) {
return MaterialApp(
title: 'Jetpack',
theme: snapshot.data.themeData,
home: PageHome(),
routes: <String, WidgetBuilder>{
"/pageChatGroup": (context) => PageChatGroup(),
"/LaoMeng": (context) => LaoMeng(),
},
);
})
- AppTheme 是資料結構
- changeTheTheme 是演算法
- StreamBuilder 是繫結UI
這樣一整套程式碼的邏輯就是我們所說的Flutter狀態管理,這樣解釋大家理解了嗎?再細說,演算法就是我們如何管理,資料結構就是資料狀態,狀態管理的本質還是如何通過合理的演算法管理資料,如何取,如何接收等,最終展示在UI上,通過UI的變更來體現狀態的管理邏輯。
為什麼需要
這裡就需要明白一個事情,Flutter的很多優秀的設計都來源於React,對於react來說,同級元件之間的通訊尤為麻煩,或者是非常麻煩了,所以我們把所有需要多個元件使用的state拿出來,整合到頂部容器,進行分發。狀態管理可以實現元件通訊、跨元件資料儲存。推薦閱讀對 React 狀態管理的理解及方案對比,那麼對於Flutter來說呢?你知道Android、Ios等原生於Flutter最本質的區別嗎?來看一段程式碼:
//android
TextView tv = TextView()
tv.setText("text")
///flutter
setState{
text = "text"
}
從上面程式碼我們看出,Android的狀態變更是通過具體的元件直接賦值,如果頁面全部變更,你是不是需要每一個都設定一遍呢?,而Flutter的變更就簡單粗暴,setState搞定,它背後的邏輯是重新build整個頁面,發現有變更,再將新的資料賦值,其實Android、Ios與flutter的本質的區別就是資料與檢視完全分離,當然Android也出現了UI繫結框架,似乎跟React、Flutter越來越像,所以這也在另一方面凸顯出了,Flutter設計的先進性,沒有什麼創新,但更符合未來感,回過頭來,仔細想一想,這樣設計有什麼弊端?
對了你猜對了:頁面如何重新整理才是Flutter的關鍵,做Android的同學肯定也面臨著一個問題,頁面的重繪導致的丟幀問題,為了更好,我們很多時候都選擇了區域性重新整理來優化對吧,Android、Ios已經很明確的告訴UI要重新整理什麼更新什麼,而對於Flutter來說,這一點很不清晰,雖然Flutter也做了類似虛擬Dom優化重繪邏輯,但這些遠遠不夠的,如何合理的更新UI才是最主要的,這個時候一大堆的狀態管理就出來了,當然狀態管理也不是僅僅為了解決更新問題。
我再丟擲一個問題,如果我有一個widget A,我想在另外一個widget B中改變widget A的一個狀態,或者從網路、資料庫取到資料,然後重新整理它,怎麼做?我們來模擬一下,來看程式碼
糟糕的狀態管理程式碼
class WidgetTest extends StatefulWidget {
@override
_WidgetTestState createState() => _WidgetTestState();
}
class _WidgetTestState extends State<WidgetTest> {
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
WidgetA(),
WidgetB()
],
),
);
}
}
_WidgetAState _widgetAState;
class WidgetA extends StatefulWidget {
@override
_WidgetAState createState() {
_widgetAState = _WidgetAState();
return _widgetAState;
}
}
class _WidgetAState extends State<WidgetA> {
var title = "";
@override
Widget build(BuildContext context) {
return Container(
child: Text(title),
);
}
}
class WidgetB extends StatefulWidget {
@override
_WidgetBState createState() => _WidgetBState();
}
class _WidgetBState extends State<WidgetB> {
@override
Widget build(BuildContext context) {
return Container(
child: RaisedButton(
onPressed: () {
_widgetAState.setState(() {
_widgetAState.title = "WidgetB";
});
},
),
);
}
}
WidgetTest頁面有兩個widget,分別是WidgetA、WidgetB,WidgetB通過RaisedButton的onPressed來改變WidgetA的Text,怎麼做到的呢,直接用WidgetA的_WidgetAState物件提供的setState函式來變更,沒什麼問題對吧,而且功能實現了,但你仔細思考一下,這有什麼問題呢?
- _WidgetAState 被全域性化,而且它所有狀態被暴漏出去,如果_WidgetAState有十個狀態,只有一個想讓別人變更,可惜已經晚了, 你加'_'也不行,元件的隱私全沒了
- 耦合變高,WidgetB有_WidgetAState的強關聯,我們編碼追求的解偶,在這裡完全被忽視了
- 效能變差,為什麼這麼說?因為每次_widgetAState.setState都會導致整個頁面甚至子Widget的重新build,如果_widgetAState裡面有成千上百的狀態,效能肯定差到極點
- 不可測,程式變得難以測試
如何變好呢
這就需要選擇一種合適的狀態管理方式。
狀態管理的目標
其實我們做狀態管理,不僅僅是因為它的特點,而為了更好架構,不是嗎?
- 程式碼要層次分明,易維護,易閱讀
- 可擴充套件,易維護,可以動態替換UI而不影響演算法邏輯
- 安全可靠,保持資料的穩定伸縮
- 效能佳,區域性優化
這些不緊緊是狀態管理的目的,也是我們做一款優秀應用的基礎架構哦。
基本分類
- 區域性管理 官方也稱 Ephemeral state,意思是短暫的狀態,這種狀態根本不需要做全域性處理
舉個例子,如下方的_index,這就是一個區域性或者短暫狀態,只需要StatefulWidget處理即可完成
class MyHomepage extends StatefulWidget {
@override
_MyHomepageState createState() => _MyHomepageState();
}
class _MyHomepageState extends State<MyHomepage> {
int _index = 0;
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: _index,
onTap: (newIndex) {
setState(() {
_index = newIndex;
});
},
// ... items ...
);
}
}
- 全域性管理 官方稱 App state,即應用狀態,非短暫狀態,您要在應用程式的許多部分之間共享,以及希望在使用者會話之間保持的狀態,就是我們所說的應用程式狀態(有時也稱為共享狀態)
例如:
- 使用者偏好
- 登入資訊
- 購物車
- 新聞閱讀狀態
狀態分類官方定義
沒有明確的通用規則來區分特定變數是短暫狀態還是應用程式狀態。有時,您必須將一個重構為另一個。例如,您將從一個明顯的短暫狀態開始,但是隨著您的應用程式功能的增長,可能需要將其移至應用程式狀態。 出於這個原因,請使用下圖進行分類:
總之,任何Flutter應用程式中都有兩種概念性的狀態型別。臨時狀態可以使用State和setState()來實現,並且通常是單個視窗小部件的本地狀態。剩下的就是您的應用狀態。兩種型別在任何Flutter應用程式中都有自己的位置,兩者之間的劃分取決於您自己的喜好和應用程式的複雜性
沒有最好的管理方式,只有最合適的管理方式
底層邏輯
底層邏輯我想告訴你的是,Flutter中目前有哪些可以做到狀態管理,有什麼缺點,適合做什麼不適合做什麼,只有你完全明白底層邏輯,才不會畏懼複雜的邏輯,即使是複雜的邏輯,你也能選擇合理的方式去管理狀態。
- State
StatefulWidget、StreamBuilder狀態管理方式
- InheritedWidget
專門負責Widget樹中資料共享的功能型Widget,如Provider、scoped_model就是基於它開發
- Notification
與InheritedWidget正好相反,InheritedWidget是從上往下傳遞資料,Notification是從下往上,但兩者都在自己的Widget樹中傳遞,無法跨越樹傳遞。
- Stream
資料流 如Bloc、flutter_redux、fish_redux等也都基於它來做實現
為什麼列這些東西?因為現在大部分流行的狀態管理都離不開它們。理解它們比理解那些吹自己牛逼的框架要好的多。請關注底層邏輯,這樣你才能遊刃有餘。下面我們一個個分析一下:
State
State 是我們常用而且使用最頻繁的一個狀態管理類,它必須結合StatefulWidget一起使用,StreamBuilder繼承自StatefulWidget,同樣是通過setState來管理狀態
舉個例子來看下:
class TapboxA extends StatefulWidget {
TapboxA({Key key}) : super(key: key);
@override
_TapboxAState createState() => _TapboxAState();
}
class _TapboxAState extends State<TapboxA> {
bool _active = false;
void _handleTap() {
setState(() {
_active = !_active;
});
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
_active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
引用官方的例子,這裡_active狀態就是通過State提供的setState函式來實現的
為什麼會讓State去管理狀態,而不是Widget本身呢?Flutter設計時讓Widget本身是不變的,類似固定的配置資訊,那麼就需要一個角色來控制它,State就出現了,但State的任何更改都會強制整個Widget重新構建,當然你也可以覆蓋必要方法自己控制邏輯。
再看個例子:
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return Container(
child: TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//------------------------- TapboxB ----------------------------------
class TapboxB extends StatelessWidget {
TapboxB({Key key, this.active: false, @required this.onChanged})
: super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
void _handleTap() {
onChanged(!active);
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
從這裡你看出什麼?對了,父元件可以通過setState來重新整理子Widget的狀態變化,所以得出如下觀點
注意
setState是整個Widget重新構建(而且子Widget也會跟著銷燬重建),這個點也是為什麼不推薦你大量使用StatefulWidget的原因。如果頁面足夠複雜,就會導致嚴重的效能損耗。如何優化呢?建議使用StreamBuilder,它原理上也是State,但它做到了子Widget的區域性重新整理,不會導致整個頁面的重建,是不是就好很多了呢?
State缺點
從上面的程式碼我們分析一下它的缺點
- 無法做到跨元件共享資料(這個跨是無關聯的,如果是直接的父子關係,我們不認為是跨元件)
setState是State的函式,一般我們會將State的子類設定為私有,所以無法做到讓別的元件呼叫State的setState函式來重新整理
- setState會成為維護的難點,因為啥哪哪都是。
隨著頁面狀態的增多,你可能在呼叫setState的地方會越來越多,不能統一管理
- 處理資料邏輯和檢視混合在一起,違反程式碼設計原則
比如資料庫的資料取出來setState到Ui上,這樣編寫程式碼,導致狀態和UI耦合在一起,不利於測試,不利於複用。
State小結
當然反過來講,不是因為它有缺點我們就不使用了,我們追求的簡單高效,簡單實現,高效執行,當複雜到需要更好的管理的時候再重構。一個基本原則就是,狀態是否需要跨元件使用,如果需要那就用別的辦法管理狀態而不是State管理。
InheritedWidget
InheritedWidget是一個無私的Widget,它可以把自己的狀態資料,無私的交給所有的子Widget,所有的子Widget可以無條件的繼承它的狀態。就這麼一個東西。有了State我們為什麼還需要它呢?我們已經知道,State是可以更新直接子Widget的狀態,但如果是子Widget的子Widget呢,所以說InheritedWidget的存在,一是為了更簡單的獲取狀態,二是大家都共享這個狀態,舉個例子
class InheritedWidgetDemo extends InheritedWidget {
final int accountId;
InheritedWidgetDemo(this.accountId, {Key key, Widget child})
: super(key: key, child: child);
@override
bool updateShouldNotify(InheritedWidgetDemo old) =>
accountId != old.accountId;
static InheritedWidgetDemo of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<InheritedWidgetDemo>();
}
}
class MyPage extends StatelessWidget {
final int accountId;
MyPage(this.accountId);
Widget build(BuildContext context) {
return new InheritedWidgetDemo(
accountId,
child: const MyWidget(),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget();
Widget build(BuildContext context) {
return MyOtherWidget();
}
}
class MyOtherWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final myInheritedWidget = InheritedWidgetDemo.of(context);
print(myInheritedWidget.accountId);
}
}
- InheritedWidgetDemo共享狀態accountId給了MyOtherWidget,而MyOtherWidget是MyWidget的子Widget,這就是InheritedWidget的功效,它可以做到跨元件共享狀態。
- const MyWidget() 表示該Widget是常量,不會因為頁面的重新整理導致重新build,這就是優化的細節,這裡想一下,如果你用State實現,不是就需要它setState才能實現MyOtherWidget的重新build,這樣做的壞處就是導致整個UI的重新整理。
- updateShouldNotify 它也是一個優化點,在你橫屏變豎屏的同時,導致整個UI重新build,可由於updateShouldNotify的判斷,系統將不會重新build MyOtherWidget,也是一種佈局優化。
- 子樹中的元件通過InheritedWidgetDemo.of(context)訪問共享狀態。
有的人想了,InheritedWidget這麼好用,那我把整個App的狀態都存進來怎麼樣?類似這樣
class AppContext {
int teamId;
String teamName;
int studentId;
String studentName;
int classId;
...
}
其實這樣不好,我們不光是要做技術上的元件化,更要關注的是業務,對業務的充分理解並實現模組化分工,在使用InheritedWidget時候特別是要注意這一點,更推薦你使用該方案:
class TeamContext {
int teamId;
String teamName;
}
class StudentContext {
int studentId;
String studentName;
}
class ClassContext {
int classId;
...
}
注意
它的資料是隻讀的,雖然很無私,但子widget不能修改,那麼如何修改呢?
舉個例子:
class Item {
String reference;
Item(this.reference);
}
class _MyInherited extends InheritedWidget {
_MyInherited({
Key key,
@required Widget child,
@required this.data,
}) : super(key: key, child: child);
final MyInheritedWidgetState data;
@override
bool updateShouldNotify(_MyInherited oldWidget) {
return true;
}
}
class MyInheritedWidget extends StatefulWidget {
MyInheritedWidget({
Key key,
this.child,
}): super(key: key);
final Widget child;
@override
MyInheritedWidgetState createState() => new MyInheritedWidgetState();
static MyInheritedWidgetState of(BuildContext context){
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
}
class MyInheritedWidgetState extends State<MyInheritedWidget>{
/// List of Items
List<Item> _items = <Item>[];
/// Getter (number of items)
int get itemsCount => _items.length;
/// Helper method to add an Item
void addItem(String reference){
setState((){
_items.add(new Item(reference));
});
}
@override
Widget build(BuildContext context){
return new _MyInherited(
data: this,
child: widget.child,
);
}
}
class MyTree extends StatefulWidget {
@override
_MyTreeState createState() => new _MyTreeState();
}
class _MyTreeState extends State<MyTree> {
@override
Widget build(BuildContext context) {
return new MyInheritedWidget(
child: new Scaffold(
appBar: new AppBar(
title: new Text('Title'),
),
body: new Column(
children: <Widget>[
new WidgetA(),
new Container(
child: new Row(
children: <Widget>[
new Icon(Icons.shopping_cart),
new WidgetB(),
new WidgetC(),
],
),
),
],
),
),
);
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return new Container(
child: new RaisedButton(
child: new Text('Add Item'),
onPressed: () {
state.addItem('new item');
},
),
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return new Text('${state.itemsCount}');
}
}
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Text('I am Widget C');
}
}
該例子引用自widget-state-context-inheritedwidget/歡迎閱讀學習哦
- _MyInherited是InheritedWidget,每次我們通過單擊“ Widget A”的按鈕新增元素時都會重新建立
- MyInheritedWidget是一個狀態為包含元素列表的視窗小部件。可通過(BuildContext上下文)的靜態MyInheritedWidgetState訪問此狀態
- MyInheritedWidgetState公開一個getter(itemsCount)和一個方法(addItem),以便子控制元件樹的一部分的控制元件可以使用它們
- 每次我們向State新增元素時,都會重新構建MyInheritedWidgetState
- MyTree類僅構建一個小部件樹,將MyInheritedWidget作為該樹的父級
- WidgetA是一個簡單的RaisedButton,按下該按鈕時,會呼叫最近的MyInheritedWidget的addItem方法。
- WidgetB是一個簡單的Text,它顯示在最接近的 MyInheritedWidget級別上顯示的元素數量
看了一下日誌輸出如圖:
有沒有發現一個問題?當MyInheritedWidgetState.addItem,導致setState被呼叫,然後就觸發了WidgetA、WidgetB的build的方法,而WidgetA根本不需要重新build,這不是浪費嗎?那麼我們如何優化呢?
static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){
return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
: context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
通過抽象rebuild屬性來控制是否需要重新build
final MyInheritedWidgetState state = MyInheritedWidget.of(context,false);
然後用的時候加以引數控制,改完程式碼,再看下日誌:
看,已經生效了。你現在是不是對InheritedWidget有了更清晰的認識了呢?但說到它就不得不提InheritedModel,它是InheritedWidget的子類,InheritedModel可以做到部分資料改變的時候才會重建,你可以修改上面例子
class _MyInheritedWidget extends InheritedModel {
static MyInheritedWidgetState of(BuildContext context, String aspect) {
return InheritedModel.inheritFrom<_MyInheritedWidget>(context, aspect: aspect).data;
}
@override
bool updateShouldNotifyDependent(_MyInheritedWidget old, Set aspects) {
return aspects.contains('true');
}
}
呼叫修改為:
///不允許重新build
final MyInheritedWidgetState state = MyInheritedWidget.of(context,"false");
///允許重新build
final MyInheritedWidgetState state = MyInheritedWidget.of(context,"true");
推薦閱讀
inheritedmodel-vs-inheritedwidget
https://juju.one/inheritedwidget-inheritedmodel/
widget-state-context-inheritedwidget/
InheritedWidget 缺點
通過上面的分析,我們來看下它的缺點
- 容易造成不必要的重新整理
- 不支援跨頁面(route)的狀態,意思是跨樹,如果不在一個樹中,我們無法獲取
- 資料是不可變的,必須結合StatefulWidget、ChangeNotifier或者Steam使用
InheritedWidget 小結
經過一系列的舉例和驗證,你也基本的掌握了InheritedWidget了吧,這個元件特別適合在同一樹型Widget中,抽象出公有狀態,每一個子Widget或者孫Widget都可以獲取該狀態,我們還可以通過手段控制rebuild的粒度來優化重繪邏輯,但它更適合從上往下傳遞,如果是從下往上傳遞,我們如何做到呢?請往下看,馬上給你解答
Notification
它是Flutter中跨層資料共享的一種機制,注意,它不是widget,它提供了dispatch方法,來讓我們沿著context對應的Element節點向上逐層傳送通知
具個簡單例子看下
class TestNotification extends Notification {
final int test;
TestNotification(this.test);
}
var a = 0;
// ignore: must_be_immutable
class WidgetNotification extends StatelessWidget {
final String btnText;
WidgetNotification({Key key, this.btnText}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: RaisedButton(
child: Text(btnText),
onPressed: () {
var b = ++a;
debugPrint(b.toString());
TestNotification(b).dispatch(context);
},
),
);
}
}
class WidgetListener extends StatefulWidget {
@override
_WidgetListenerState createState() => _WidgetListenerState();
}
class _WidgetListenerState extends State<WidgetListener> {
int _test = 1;
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
NotificationListener<TestNotification>(
child: Column(
children: <Widget>[
Text("監聽$_test"),
WidgetNotification(btnText: "子Widget",)
],
),
onNotification: (TestNotification notification) {
setState(() {
_test = notification.test;
});
return true;
},
),
WidgetNotification(btnText: "非子Widget",)
],
),
);
}
}
- 定義TestNotification通知的實現
- WidgetNotification 負責通知結果,通過RaisedButton的點選事件,將資料a傳遞出去,通過Notification提供的dispatch方法向上傳遞
- WidgetListener通過Widget NotificationListener來監聽資料變化,最終通過setState變更資料
- WidgetNotification 例項化了兩次,一次在NotificationListener的樹內部,一個在NotificationListener的外部,經過測試發現,在外部的WidgetNotification並不能通知到內容變化。
所以說在使用Notification的時候要注意,如果遇到無法收到通知的情況,考慮是否是Notification 未在NotificationListener的內部發出通知,這個一定要注意。
同樣的思路,我想看下Notification是如何重新整理Ui的
在程式碼里加入了跟通知無關緊要的WidgetC
這麼看來,你以為是Notification導致的嗎?我把這個註釋掉,如圖
再執行看下,連續點選了八次
原來是State的原因,那麼這種情況我們如何優化呢?這就用到了Stream了,請接著往下繼續看哦。
推薦閱讀
flutter-notifications-bubble-up-and-values-go-down
Notification缺點
- 不支援跨頁面(route)的狀態,準備的說不支援NotificationListener同級或者父級Widget的狀態通知
- 本身不支援重新整理UI,需要結合State使用
- 如果結合State,會導致整個UI的重繪,效率底下不科學
Notification小結
使用起來很簡單,但在重新整理UI方面需要注意,如果頁面複雜度很高,導致無關緊要的元件跟著重新整理,得不償失,還需要另找蹊徑,躲開這些坑,下面我來介紹如何完美躲閃,重磅來襲Stream。
Stream
Stream其實就是一個生產者消費者模型,一端負責生產,一端負責消費,而且是純Dart的實現,跟Flutter沒什麼關係,扯上關係的就是用StreamBuilder來構建一個Stream通道的Widget,像知名的rxdart、BloC、flutter_redux全都用到了Stream的api。所以學習它才是我們掌握狀態管理的一個關鍵
我們先來看下如何改造上面Notification的例子達到我們想要的重新整理效果
///step1
class TestBloc {
final _testStreamController = StreamController<int>();
get changeTest => _testStreamController.sink.add;
get testStream => _testStreamController.stream;
dispose() {
_testStreamController.close();
}
}
final testBloc = TestBloc();
/// step2
StreamBuilder<int>(
initialData: 0,
stream: testBloc.testStream,
builder: (context, snapshot) {
return Text("監聽${snapshot.data}");
}
)
/// step3
onNotification: (TestNotification notification) {
testBloc.changeTest(notification.test);
return true;
}
- 第一步定義TestBloc,負責管理Stream,提供changeTest函式來往Stream通道新增元素
- 將TestBloc的流testStream賦值給StreamBuilder的stream屬性加以繫結
- 在收到通知的地方用 testBloc.changeTest來往stream中新增元素,最終由StreamBuilder的setState更新,對的StreamBuilder是通過State重新整理的,想深入的可以看下面推薦閱讀內容,講的很好
看下執行效果
看見了吧,無關緊要的Widget已經不跟著重新整理了,這就是Stream的重要性
推薦閱讀
Stream 缺點
再好的東西,我們更應該關注下它的缺點
- api生澀,不好理解
- 需要定製化,才能滿足更復雜的場景
- 沒有自動dispose邏輯 我們做開發都有個習慣,當這個流不被使用的時候,喜歡close掉,可惜Stream並沒有提供這樣的api,需要自己擴充套件實現,大部分人都是使用StatefulWidget的dispose函式來輔助流的close呼叫,那我們不想使用StatefulWidget怎麼辦,感覺它寫法太麻煩
我們如何做到讓Stream自動Close掉呢?
- 一種辦法是自己實現擴充套件StreamBuilder,在它dispose的時候呼叫,因為StreamBuilder是Statefulwidget的子類可以覆蓋dispose函式
- 第二是參考Provider的實現,經過原始碼分析Provider的原理是依賴於InheritedElement的unmount函式實現的,最終回撥函式dispose,unmount函式類似於Android Activity的Destroy函式,頁面徹底銷燬了,不需要任何資料資源了,都釋放了得了,InheritedElement是InheritedWidget的虛擬Dom物件,Flutter 頁面繪製三板斧嘛(widget、element、renderObject)
第二種不太現實,實現起來邏輯複雜,但我想告訴有這麼一個思路,你可以參考。
缺點恰恰是它的優點,保證了足夠靈活,你更可基於它做一個好的設計,滿足當下業務的設計。
小結
通過對State、InheritedWidget、Notification、Stream的學習,你是不是覺得,Flutter的狀態管理也就這些了呢?不一定哈,我也在不斷的學習,如果碰到新的技術,繼續分享給你們哦。難道這就完了嗎?沒有,其實我們只是學了第一步,是什麼,如何用,還沒有討論怎麼用好呢?需要什麼標準嗎,當然有,下面通過我的專案實戰經驗來提出一個基本原則,超過這個原則你就是在破壞平衡,請往下看。
狀態管理的使用原則
區域性管理優於全域性
這個原則來源於,Flutter的效能優化,區域性重新整理肯定比全域性重新整理要好很多,那麼我們在管理狀態的同時,也要考慮該狀態到底是區域性還是全域性,從而編寫正確的邏輯。
保持資料安全性
用“_”私有化狀態,因為當開發人員眾多,當別人看到你的變數的時候,第一反應可能不是找你提供的方法,而是直接對變數操作,那就有可能出現想不到的後果,如果他只能呼叫你提供的方法,那他就要遵循你方法的邏輯,避免資料被處理錯誤。
考慮頁面重新build帶來的影響
很多時候頁面的重建都會呼叫build函式,也就是說,在一個生命週期內,build函式是多次被呼叫的,所以你就要考慮資料的初始化或者重新整理怎麼樣才能合理。
使用成熟狀態管理庫弊端
- 增加程式碼複雜性
- 框架bug修復需要時間等待
- 不理解框架原理導致使用方式不對,反而帶來更多問題
- 選型錯誤導致不符合應用要求
- 與團隊風格衝突不適用
通過了解它們的弊端來規避一些風險,綜合考慮,選框架不易,且行且珍惜。
選型原則
- 侵入性
- 擴充套件性
- 高效能
- 安全性
- 駕馭性
- 易用性
- 範圍性
所有的框架都有侵入性,你同意嗎?不同意請左轉,前面有個坑,你可以跳過去。目前侵入性比較高的代表ScopedModel,為啥?因為它是用extend實現的,需要繼承實現的基本不是什麼好實現,你同意嗎?同上。
擴充套件性就不用說了,如果你選擇的框架只能使用它提供的幾個入口,那麼請你放棄使用它。高效能也是很重要的,這個需要明白它的原理,看它到底如何做的管理。安全性也很重要,看他資料管理通道是否安全穩定。駕馭性,你說你都不理解你就敢用,出了問題找誰?如果駕馭不了也不要用。易用性大家應該都明白,如果用它一個框架需要N多配置,N多實現,放棄吧,不合適。簡單才是硬道理。
範圍性
這個特點是flutter中比較明顯的,框架選型一定要考慮框架的適用範圍,到底是適合做區域性管理,還是適合全域性管理,要做一個實際的考量。
推薦用法
如果是初期,建議多使用Stream、State、Notification來自行處理,順便學習原始碼,多理解,多實踐。有架構能力的就可以著手封裝了,提供更簡單的使用方式
如果是後期,當然也是在前面的基礎之上,再去考慮使用Provider、redux等複雜的框架,原則上要吃透原始碼,否則不建議使用。
注意
你以為使用框架就能萬事大吉了?效能優化是一個不變的話題,包括Provider在內的,如果你使用不當,照樣出現頁面的效能損耗嚴重,所以你又回到了為啥會這樣,請你學習上面的底層邏輯,謝謝?
總結
通過這期分享,你是不是對Flutter的狀態管理有了一個重新的認識呢?如果對你有幫住,請點一下下面的贊哦。謝謝?。