前言
2019 Google I/O 大會,google就推出Provider,成為官方推薦的狀態(tài)管理方式之一,F(xiàn)lutter 狀態(tài)管理一直是個很熱門的話題,而且狀態(tài)管理的庫也是超級多,這確實是我們每一個做Flutter開發(fā)難以避免的一道坎,既然這么重要,我們?nèi)绾稳ダ斫馑绾问褂盟绾巫龅礁媚兀拷酉聛碜屛腋嬖V你答案
主要內(nèi)容
一張圖告訴你,我要講的主要內(nèi)容。下面將圍繞這八個方面來講。七個理論,一個實踐。
- 狀態(tài)管理是什么 (已完成)
- 為什么需要狀態(tài)管理(已完成)
- 狀態(tài)管理基本分類(已完成)
- 狀態(tài)管理的底層邏輯(已完成)
- 狀態(tài)管理的使用原則(已完成)
- 使用成熟狀態(tài)管理庫的弊端(已完成)
- 選擇狀態(tài)管理庫的原則(已完成)
- Provider 深入分析(學(xué)以致用)(已完成)
狀態(tài)管理是什么
我們知道最基本的程序是什么:
-
程序=算法+數(shù)據(jù)結(jié)構(gòu)
數(shù)據(jù)是程序的中心。數(shù)據(jù)結(jié)構(gòu)和算法兩個概念間的邏輯關(guān)系貫穿了整個程序世界,首先二者表現(xiàn)為不可分割的關(guān)系。其實Flutter不就是一個程序嗎,那我們面臨的最底層的問題還是算法和數(shù)據(jù)結(jié)構(gòu),所以我們推導(dǎo)出 -
Flutter=算法+數(shù)據(jù)結(jié)構(gòu)
那狀態(tài)管理是什么?我也用公式來表達(dá)一下,如下: -
Flutter狀態(tài)管理=算法+數(shù)據(jù)結(jié)構(gòu)+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 是數(shù)據(jù)結(jié)構(gòu)
- changeTheTheme 是算法
- StreamBuilder 是綁定UI
這樣一整套代碼的邏輯就是我們所說的Flutter狀態(tài)管理,這樣解釋大家理解了嗎?再細(xì)說,算法就是我們?nèi)绾喂芾恚瑪?shù)據(jù)結(jié)構(gòu)就是數(shù)據(jù)狀態(tài),狀態(tài)管理的本質(zhì)還是如何通過合理的算法管理數(shù)據(jù),如何取,如何接收等,最終展示在UI上,通過UI的變更來體現(xiàn)狀態(tài)的管理邏輯。
為什么需要
這里就需要明白一個事情,F(xiàn)lutter的很多優(yōu)秀的設(shè)計都來源于React,對于react來說,同級組件之間的通信尤為麻煩,或者是非常麻煩了,所以我們把所有需要多個組件使用的state拿出來,整合到頂部容器,進行分發(fā)。狀態(tài)管理可以實現(xiàn)組件通信、跨組件數(shù)據(jù)儲存。推薦閱讀對 React 狀態(tài)管理的理解及方案對比,那么對于Flutter來說呢?你知道Android、Ios等原生于Flutter最本質(zhì)的區(qū)別嗎?來看一段代碼:
//android
TextView tv = TextView()
tv.setText("text")
///flutter
setState{
text = "text"
}
從上面代碼我們看出,Android的狀態(tài)變更是通過具體的組件直接賦值,如果頁面全部變更,你是不是需要每一個都設(shè)置一遍呢?,而Flutter的變更就簡單粗暴,setState搞定,它背后的邏輯是重新build整個頁面,發(fā)現(xiàn)有變更,再將新的數(shù)據(jù)賦值,其實Android、Ios與flutter的本質(zhì)的區(qū)別就是數(shù)據(jù)與視圖完全分離,當(dāng)然Android也出現(xiàn)了UI綁定框架,似乎跟React、Flutter越來越像,所以這也在另一方面凸顯出了,F(xiàn)lutter設(shè)計的先進性,沒有什么創(chuàng)新,但更符合未來感,回過頭來,仔細(xì)想一想,這樣設(shè)計有什么弊端?
對了你猜對了:頁面如何刷新才是Flutter的關(guān)鍵,做Android的同學(xué)肯定也面臨著一個問題,頁面的重繪導(dǎo)致的丟幀問題,為了更好,我們很多時候都選擇了局部刷新來優(yōu)化對吧,Android、Ios已經(jīng)很明確的告訴UI要刷新什么更新什么,而對于Flutter來說,這一點很不清晰,雖然Flutter也做了類似虛擬Dom優(yōu)化重繪邏輯,但這些遠(yuǎn)遠(yuǎn)不夠的,如何合理的更新UI才是最主要的,這個時候一大堆的狀態(tài)管理就出來了,當(dāng)然狀態(tài)管理也不是僅僅為了解決更新問題。
我再拋出一個問題,如果我有一個widget A,我想在另外一個widget B中改變widget A的一個狀態(tài),或者從網(wǎng)絡(luò)、數(shù)據(jù)庫取到數(shù)據(jù),然后刷新它,怎么做?我們來模擬一下,來看代碼
糟糕的狀態(tài)管理代碼
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函數(shù)來變更,沒什么問題對吧,而且功能實現(xiàn)了,但你仔細(xì)思考一下,這有什么問題呢?
- _WidgetAState 被全局化,而且它所有狀態(tài)被暴漏出去,如果WidgetAState有十個狀態(tài),只有一個想讓別人變更,可惜已經(jīng)晚了, 你加''也不行,組件的隱私全沒了
- 耦合變高,WidgetB有_WidgetAState的強關(guān)聯(lián),我們編碼追求的解偶,在這里完全被忽視了
- 性能變差,為什么這么說?因為每次_widgetAState.setState都會導(dǎo)致整個頁面甚至子Widget的重新build,如果_widgetAState里面有成千上百的狀態(tài),性能肯定差到極點
- 不可測,程序變得難以測試
如何變好呢
這就需要選擇一種合適的狀態(tài)管理方式。
狀態(tài)管理的目標(biāo)
其實我們做狀態(tài)管理,不僅僅是因為它的特點,而為了更好架構(gòu),不是嗎?
- 代碼要層次分明,易維護,易閱讀
- 可擴展,易維護,可以動態(tài)替換UI而不影響算法邏輯
- 安全可靠,保持?jǐn)?shù)據(jù)的穩(wěn)定伸縮
- 性能佳,局部優(yōu)化
這些不緊緊是狀態(tài)管理的目的,也是我們做一款優(yōu)秀應(yīng)用的基礎(chǔ)架構(gòu)哦。
基本分類
- 局部管理 官方也稱 Ephemeral state,意思是短暫的狀態(tài),這種狀態(tài)根本不需要做全局處理
舉個例子,如下方的_index,這就是一個局部或者短暫狀態(tài),只需要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,即應(yīng)用狀態(tài),非短暫狀態(tài),您要在應(yīng)用程序的許多部分之間共享,以及希望在用戶會話之間保持的狀態(tài),就是我們所說的應(yīng)用程序狀態(tài)(有時也稱為共享狀態(tài))
例如:- 用戶偏好
- 登錄信息
- 購物車
- 新聞閱讀狀態(tài)
狀態(tài)分類官方定義
沒有明確的通用規(guī)則來區(qū)分特定變量是短暫狀態(tài)還是應(yīng)用程序狀態(tài)。有時,您必須將一個重構(gòu)為另一個。例如,您將從一個明顯的短暫狀態(tài)開始,但是隨著您的應(yīng)用程序功能的增長,可能需要將其移至應(yīng)用程序狀態(tài)。 出于這個原因,請使用下圖進行分類:
總之,任何Flutter應(yīng)用程序中都有兩種概念性的狀態(tài)類型。臨時狀態(tài)可以使用State和setState()來實現(xiàn),并且通常是單個窗口小部件的本地狀態(tài)。剩下的就是您的應(yīng)用狀態(tài)。兩種類型在任何Flutter應(yīng)用程序中都有自己的位置,兩者之間的劃分取決于您自己的喜好和應(yīng)用程序的復(fù)雜性
沒有最好的管理方式,只有最合適的管理方式
底層邏輯
底層邏輯我想告訴你的是,F(xiàn)lutter中目前有哪些可以做到狀態(tài)管理,有什么缺點,適合做什么不適合做什么,只有你完全明白底層邏輯,才不會畏懼復(fù)雜的邏輯,即使是復(fù)雜的邏輯,你也能選擇合理的方式去管理狀態(tài)。
- State
StatefulWidget、StreamBuilder狀態(tài)管理方式 - InheritedWidget
專門負(fù)責(zé)Widget樹中數(shù)據(jù)共享的功能型Widget,如Provider、scoped_model就是基于它開發(fā) - Notification
與InheritedWidget正好相反,InheritedWidget是從上往下傳遞數(shù)據(jù),Notification是從下往上,但兩者都在自己的Widget樹中傳遞,無法跨越樹傳遞。 - Stream
數(shù)據(jù)流 如Bloc、flutter_redux、fish_redux等也都基于它來做實現(xiàn)
為什么列這些東西?因為現(xiàn)在大部分流行的狀態(tài)管理都離不開它們。理解它們比理解那些吹自己牛逼的框架要好的多。請關(guān)注底層邏輯,這樣你才能游刃有余。下面我們一個個分析一下:
State
State 是我們常用而且使用最頻繁的一個狀態(tài)管理類,它必須結(jié)合StatefulWidget一起使用,StreamBuilder繼承自StatefulWidget,同樣是通過setState來管理狀態(tài)
舉個例子來看下:
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狀態(tài)就是通過State提供的setState函數(shù)來實現(xiàn)的
為什么會讓State去管理狀態(tài),而不是Widget本身呢?Flutter設(shè)計時讓W(xué)idget本身是不變的,類似固定的配置信息,那么就需要一個角色來控制它,State就出現(xiàn)了,但State的任何更改都會強制整個Widget重新構(gòu)建,當(dāng)然你也可以覆蓋必要方法自己控制邏輯。
再看個例子:
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的狀態(tài)變化,所以得出如下觀點
注意
setState是整個Widget重新構(gòu)建(而且子Widget也會跟著銷毀重建),這個點也是為什么不推薦你大量使用StatefulWidget的原因。如果頁面足夠復(fù)雜,就會導(dǎo)致嚴(yán)重的性能損耗。如何優(yōu)化呢?建議使用StreamBuilder,它原理上也是State,但它做到了子Widget的局部刷新,不會導(dǎo)致整個頁面的重建,是不是就好很多了呢?
State缺點
從上面的代碼我們分析一下它的缺點
- 無法做到跨組件共享數(shù)據(jù)(這個跨是無關(guān)聯(lián)的,如果是直接的父子關(guān)系,我們不認(rèn)為是跨組件)
setState是State的函數(shù),一般我們會將State的子類設(shè)置為私有,所以無法做到讓別的組件調(diào)用State的setState函數(shù)來刷新 - setState會成為維護的難點,因為啥哪哪都是。
隨著頁面狀態(tài)的增多,你可能在調(diào)用setState的地方會越來越多,不能統(tǒng)一管理 - 處理數(shù)據(jù)邏輯和視圖混合在一起,違反代碼設(shè)計原則
比如數(shù)據(jù)庫的數(shù)據(jù)取出來setState到Ui上,這樣編寫代碼,導(dǎo)致狀態(tài)和UI耦合在一起,不利于測試,不利于復(fù)用。
State小結(jié)
當(dāng)然反過來講,不是因為它有缺點我們就不使用了,我們追求的簡單高效,簡單實現(xiàn),高效運行,當(dāng)復(fù)雜到需要更好的管理的時候再重構(gòu)。一個基本原則就是,狀態(tài)是否需要跨組件使用,如果需要那就用別的辦法管理狀態(tài)而不是State管理。
InheritedWidget
InheritedWidget是一個無私的Widget,它可以把自己的狀態(tài)數(shù)據(jù),無私的交給所有的子Widget,所有的子Widget可以無條件的繼承它的狀態(tài)。就這么一個東西。有了State我們?yōu)槭裁催€需要它呢?我們已經(jīng)知道,State是可以更新直接子Widget的狀態(tài),但如果是子Widget的子Widget呢,所以說InheritedWidget的存在,一是為了更簡單的獲取狀態(tài),二是大家都共享這個狀態(tài),舉個例子
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共享狀態(tài)accountId給了MyOtherWidget,而MyOtherWidget是MyWidget的子Widget,這就是InheritedWidget的功效,它可以做到跨組件共享狀態(tài)。
- const MyWidget() 表示該Widget是常量,不會因為頁面的刷新導(dǎo)致重新build,這就是優(yōu)化的細(xì)節(jié),這里想一下,如果你用State實現(xiàn),不是就需要它setState才能實現(xiàn)MyOtherWidget的重新build,這樣做的壞處就是導(dǎo)致整個UI的刷新。
- updateShouldNotify 它也是一個優(yōu)化點,在你橫屏變豎屏的同時,導(dǎo)致整個UI重新build,可由于updateShouldNotify的判斷,系統(tǒng)將不會重新build MyOtherWidget,也是一種布局優(yōu)化。
- 子樹中的組件通過InheritedWidgetDemo.of(context)訪問共享狀態(tài)。
有的人想了,InheritedWidget這么好用,那我把整個App的狀態(tài)都存進來怎么樣?類似這樣
class AppContext {
int teamId;
String teamName;
int studentId;
String studentName;
int classId;
...
}
其實這樣不好,我們不光是要做技術(shù)上的組件化,更要關(guān)注的是業(yè)務(wù),對業(yè)務(wù)的充分理解并實現(xiàn)模塊化分工,在使用InheritedWidget時候特別是要注意這一點,更推薦你使用該方案:
class TeamContext {
int teamId;
String teamName;
}
class StudentContext {
int studentId;
String studentName;
}
class ClassContext {
int classId;
...
}
注意
它的數(shù)據(jù)是只讀的,雖然很無私,但子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/歡迎閱讀學(xué)習(xí)哦
- _MyInherited是InheritedWidget,每次我們通過單擊“ Widget A”的按鈕添加元素時都會重新創(chuàng)建
- MyInheritedWidget是一個狀態(tài)為包含元素列表的窗口小部件。可通過(BuildContext上下文)的靜態(tài)MyInheritedWidgetState訪問此狀態(tài)
- MyInheritedWidgetState公開一個getter(itemsCount)和一個方法(addItem),以便子控件樹的一部分的控件可以使用它們
- 每次我們向State添加元素時,都會重新構(gòu)建MyInheritedWidgetState
- MyTree類僅構(gòu)建一個小部件樹,將MyInheritedWidget作為該樹的父級
- WidgetA是一個簡單的RaisedButton,按下該按鈕時,會調(diào)用最近的MyInheritedWidget的addItem方法。
- WidgetB是一個簡單的Text,它顯示在最接近的 MyInheritedWidget級別上顯示的元素數(shù)量
看了一下日志輸出如圖:
有沒有發(fā)現(xiàn)一個問題?當(dāng)MyInheritedWidgetState.addItem,導(dǎo)致setState被調(diào)用,然后就觸發(fā)了WidgetA、WidgetB的build的方法,而WidgetA根本不需要重新build,這不是浪費嗎?那么我們?nèi)绾蝺?yōu)化呢?
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);
然后用的時候加以參數(shù)控制,改完代碼,再看下日志:
看,已經(jīng)生效了。你現(xiàn)在是不是對InheritedWidget有了更清晰的認(rèn)識了呢?但說到它就不得不提InheritedModel,它是InheritedWidget的子類,InheritedModel可以做到部分?jǐn)?shù)據(jù)改變的時候才會重建,你可以修改上面例子
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');
}
}
調(diào)用修改為:
///不允許重新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)的狀態(tài),意思是跨樹,如果不在一個樹中,我們無法獲取
- 數(shù)據(jù)是不可變的,必須結(jié)合StatefulWidget、ChangeNotifier或者Steam使用
InheritedWidget 小結(jié)
經(jīng)過一系列的舉例和驗證,你也基本的掌握了InheritedWidget了吧,這個組件特別適合在同一樹型Widget中,抽象出公有狀態(tài),每一個子Widget或者孫Widget都可以獲取該狀態(tài),我們還可以通過手段控制rebuild的粒度來優(yōu)化重繪邏輯,但它更適合從上往下傳遞,如果是從下往上傳遞,我們?nèi)绾巫龅侥兀空埻驴矗R上給你解答
Notification
它是Flutter中跨層數(shù)據(jù)共享的一種機制,注意,它不是widget,它提供了dispatch方法,來讓我們沿著context對應(yīng)的Element節(jié)點向上逐層發(fā)送通知
具個簡單例子看下
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("監(jiān)聽$_test"),
WidgetNotification(btnText: "子Widget",)
],
),
onNotification: (TestNotification notification) {
setState(() {
_test = notification.test;
});
return true;
},
),
WidgetNotification(btnText: "非子Widget",)
],
),
);
}
}
- 定義TestNotification通知的實現(xiàn)
- WidgetNotification 負(fù)責(zé)通知結(jié)果,通過RaisedButton的點擊事件,將數(shù)據(jù)a傳遞出去,通過Notification提供的dispatch方法向上傳遞
- WidgetListener通過Widget NotificationListener來監(jiān)聽數(shù)據(jù)變化,最終通過setState變更數(shù)據(jù)
- WidgetNotification 實例化了兩次,一次在NotificationListener的樹內(nèi)部,一個在NotificationListener的外部,經(jīng)過測試發(fā)現(xiàn),在外部的WidgetNotification并不能通知到內(nèi)容變化。
所以說在使用Notification的時候要注意,如果遇到無法收到通知的情況,考慮是否是Notification 未在NotificationListener的內(nèi)部發(fā)出通知,這個一定要注意。
同樣的思路,我想看下Notification是如何刷新Ui的
在代碼里加入了跟通知無關(guān)緊要的WidgetC
這么看來,你以為是Notification導(dǎo)致的嗎?我把這個注釋掉,如圖
再運行看下,連續(xù)點擊了八次
原來是State的原因,那么這種情況我們?nèi)绾蝺?yōu)化呢?這就用到了Stream了,請接著往下繼續(xù)看哦。
推薦閱讀
flutter-notifications-bubble-up-and-values-go-down
Notification缺點
- 不支持跨頁面(route)的狀態(tài),準(zhǔn)備的說不支持NotificationListener同級或者父級Widget的狀態(tài)通知
- 本身不支持刷新UI,需要結(jié)合State使用
- 如果結(jié)合State,會導(dǎo)致整個UI的重繪,效率底下不科學(xué)
Notification小結(jié)
使用起來很簡單,但在刷新UI方面需要注意,如果頁面復(fù)雜度很高,導(dǎo)致無關(guān)緊要的組件跟著刷新,得不償失,還需要另找蹊徑,躲開這些坑,下面我來介紹如何完美躲閃,重磅來襲Stream。
Stream
Stream其實就是一個生產(chǎn)者消費者模型,一端負(fù)責(zé)生產(chǎn),一端負(fù)責(zé)消費,而且是純Dart的實現(xiàn),跟Flutter沒什么關(guān)系,扯上關(guān)系的就是用StreamBuilder來構(gòu)建一個Stream通道的Widget,像知名的rxdart、BloC、flutter_redux全都用到了Stream的api。所以學(xué)習(xí)它才是我們掌握狀態(tài)管理的一個關(guān)鍵
我們先來看下如何改造上面Notification的例子達(dá)到我們想要的刷新效果
///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("監(jiān)聽${snapshot.data}");
}
)
/// step3
onNotification: (TestNotification notification) {
testBloc.changeTest(notification.test);
return true;
}
- 第一步定義TestBloc,負(fù)責(zé)管理Stream,提供changeTest函數(shù)來往Stream通道添加元素
- 將TestBloc的流testStream賦值給StreamBuilder的stream屬性加以綁定
- 在收到通知的地方用 testBloc.changeTest來往stream中添加元素,最終由StreamBuilder的setState更新,對的StreamBuilder是通過State刷新的,想深入的可以看下面推薦閱讀內(nèi)容,講的很好
看下運行效果
看見了吧,無關(guān)緊要的Widget已經(jīng)不跟著刷新了,這就是Stream的重要性
推薦閱讀
Stream 缺點
再好的東西,我們更應(yīng)該關(guān)注下它的缺點
- api生澀,不好理解
- 需要定制化,才能滿足更復(fù)雜的場景
- 沒有自動dispose邏輯 我們做開發(fā)都有個習(xí)慣,當(dāng)這個流不被使用的時候,喜歡close掉,可惜Stream并沒有提供這樣的api,需要自己擴展實現(xiàn),大部分人都是使用StatefulWidget的dispose函數(shù)來輔助流的close調(diào)用,那我們不想使用StatefulWidget怎么辦,感覺它寫法太麻煩
我們?nèi)绾巫龅阶孲tream自動Close掉呢?
- 一種辦法是自己實現(xiàn)擴展StreamBuilder,在它dispose的時候調(diào)用,因為StreamBuilder是Statefulwidget的子類可以覆蓋dispose函數(shù)
- 第二是參考Provider的實現(xiàn),經(jīng)過源碼分析Provider的原理是依賴于InheritedElement的unmount函數(shù)實現(xiàn)的,最終回調(diào)函數(shù)dispose,unmount函數(shù)類似于Android Activity的Destroy函數(shù),頁面徹底銷毀了,不需要任何數(shù)據(jù)資源了,都釋放了得了,InheritedElement是InheritedWidget的虛擬Dom對象,F(xiàn)lutter 頁面繪制三板斧嘛(widget、element、renderObject)
第二種不太現(xiàn)實,實現(xiàn)起來邏輯復(fù)雜,但我想告訴有這么一個思路,你可以參考。
缺點恰恰是它的優(yōu)點,保證了足夠靈活,你更可基于它做一個好的設(shè)計,滿足當(dāng)下業(yè)務(wù)的設(shè)計。
小結(jié)
你有沒有發(fā)現(xiàn)一個問題,似乎InheritedWidget、Notification、Stream的狀態(tài)更新都沒有離開State的支持?所以說Flutter在抽象功能的真的是做到了極致,InheritedWidget、Notification、Stream只是適合在不同的場景使用,而真正的狀態(tài)變更還是需要State負(fù)責(zé)。通過對State、InheritedWidget、Notification、Stream的學(xué)習(xí),你是不是覺得,F(xiàn)lutter的狀態(tài)管理也就這些了呢?不一定哈,我也在不斷的學(xué)習(xí),如果碰到新的技術(shù),繼續(xù)分享給你們哦。難道這就完了嗎?沒有,其實我們只是學(xué)了第一步,是什么,如何用,還沒有討論怎么用好呢?需要什么標(biāo)準(zhǔn)嗎,當(dāng)然有,下面通過我的項目實戰(zhàn)經(jīng)驗來提出一個基本原則,超過這個原則你就是在破壞平衡,請往下看。
狀態(tài)管理的使用原則
局部管理優(yōu)于全局
這個原則來源于,F(xiàn)lutter的性能優(yōu)化,局部刷新肯定比全局刷新要好很多,那么我們在管理狀態(tài)的同時,也要考慮該狀態(tài)到底是局部還是全局,從而編寫正確的邏輯。
保持?jǐn)?shù)據(jù)安全性
用“_”私有化狀態(tài),因為當(dāng)開發(fā)人員眾多,當(dāng)別人看到你的變量的時候,第一反應(yīng)可能不是找你提供的方法,而是直接對變量操作,那就有可能出現(xiàn)想不到的后果,如果他只能調(diào)用你提供的方法,那他就要遵循你方法的邏輯,避免數(shù)據(jù)被處理錯誤。
考慮頁面重新build帶來的影響
很多時候頁面的重建都會調(diào)用build函數(shù),也就是說,在一個生命周期內(nèi),build函數(shù)是多次被調(diào)用的,所以你就要考慮數(shù)據(jù)的初始化或者刷新怎么樣才能合理。
使用成熟狀態(tài)管理庫弊端
- 增加代碼復(fù)雜性
- 框架本身bug修復(fù)需要時間等待
- 不理解框架原理導(dǎo)致使用方式不對,反而帶來更多問題
- 選型錯誤導(dǎo)致不符合應(yīng)用要求
- 與團隊風(fēng)格沖突不適用
通過了解它們的弊端來規(guī)避一些風(fēng)險,綜合考慮,選框架不易,且行且珍惜。
選型原則
- 侵入性
- 擴展性
- 高性能
- 安全性
- 駕馭性
- 易用性
- 范圍性
所有的框架都有侵入性,你同意嗎?不同意請左轉(zhuǎn),前面有個坑,你可以跳過去。目前侵入性比較高的代表ScopedModel,為啥?因為它是用extend實現(xiàn)的,需要繼承實現(xiàn)的基本不是什么好實現(xiàn),你同意嗎?同上。
擴展性就不用說了,如果你選擇的框架只能使用它提供的幾個入口,那么請你放棄使用它。高性能也是很重要的,這個需要明白它的原理,看它到底如何做的管理。安全性也很重要,看他數(shù)據(jù)管理通道是否安全穩(wěn)定。駕馭性,你說你都不理解你就敢用,出了問題找誰?如果駕馭不了也不要用。易用性大家應(yīng)該都明白,如果用它一個框架需要N多配置,N多實現(xiàn),放棄吧,不合適。簡單才是硬道理。
范圍性
這個特點是flutter中比較明顯的,框架選型一定要考慮框架的適用范圍,到底是適合做局部管理,還是適合全局管理,要做一個實際的考量。
推薦用法
如果是初期,建議多使用Stream、State、Notification來自行處理,順便學(xué)習(xí)源碼,多理解,多實踐。有架構(gòu)能力的就可以著手封裝了,提供更簡單的使用方式
如果是后期,當(dāng)然也是在前面的基礎(chǔ)之上,再去考慮使用Provider、redux等復(fù)雜的框架,原則上要吃透源碼,否則不建議使用。
注意
你以為使用框架就能萬事大吉了?性能優(yōu)化是一個不變的話題,包括Provider在內(nèi)的,如果你使用不當(dāng),照樣出現(xiàn)頁面的性能損耗嚴(yán)重,所以你又回到了為啥會這樣,讓我們一起徹徹底底的搞明白Provider的原理,你就知道了
Provider 深入源碼分析
Flutter Provider 迄今為止最深、最全、最新的源碼分析
總結(jié)
通過這期分享,你是不是對Flutter的狀態(tài)管理有了一個重新的認(rèn)識呢?如果對你有幫住,請點一下下面的贊哦。謝謝??。