利用 Flutter 內(nèi)置的許多控件我們可以打造出一款不僅漂亮而且完美跨平臺(tái)的 App 外殼偏序,我利用其特性完成了類似知乎App的UI界面筑煮,然而一款完整的應(yīng)用程序顯然不止有外殼這么簡單。填充在外殼里面的是數(shù)據(jù)盔腔,數(shù)據(jù)來源或從本地闸翅,或從云端贵扰,大量的數(shù)據(jù)處理很容易造成數(shù)據(jù)的混亂,耦合度提高奠衔,不便于維護(hù)谆刨,于是誕生了很多設(shè)計(jì)模式和狀態(tài)管理的方式塘娶。
目前 Flutter 常用狀態(tài)管理方式有如下幾種:
- ScopedModel
- BLoC (Business Logic Component) / Rx
- Redux
這篇文章暫且不提這些比較復(fù)雜的模式。我們簡單的提出三個(gè)問題:
- Flutter 中組件之間如何通信痊夭?
- 更新 State 后組件以何種方式重新渲染刁岸?
- 如何在路由轉(zhuǎn)換之間保持狀態(tài)同步?
初探 State
我以創(chuàng)建新項(xiàng)目 Flutter 給我們默認(rèn)的計(jì)數(shù)器應(yīng)用為例她我,通過路由我將其拆分為兩部分 MyHomePage
和 PageTwo
虹曙,
MyHomePage,持有一個(gè)_counter
變量和一個(gè)增加計(jì)數(shù)的方法番舆;PageTwo酝碳,接收兩個(gè)參數(shù)(計(jì)數(shù)的至和增加計(jì)數(shù)的方法):
class PageTwo extends StatefulWidget {
final int count;
final Function increment;
const PageTwo({Key key, this.count, this.increment}) : super(key: key);
_PageTwoState createState() => _PageTwoState();
}
class _PageTwoState extends State<PageTwo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Page Two"),
),
body: Center(
child: Text(widget.count.toString(), style: TextStyle(fontSize: 30.0),),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: widget.increment,
),
);
}
}
出現(xiàn)的狀況是:我們?cè)谑醉擖c(diǎn)擊按鈕觸發(fā)計(jì)數(shù)器增加,路由到 PageTwo 后合蔽,數(shù)值正常顯示击敌,然而點(diǎn)擊這個(gè)界面中的 add 按鈕該頁面的數(shù)值并未發(fā)生改變,通過觀察父頁面的 count 值確實(shí)發(fā)生了改變拴事,因此再次通過路由到第二個(gè)界面界面才顯示正常沃斤。解答上面三個(gè)問題:
-
Flutter 中組件之間如何通信?
參數(shù)傳遞刃宵。
-
更新 State 后組件以何種方式重新渲染衡瓶?
只渲染當(dāng)前的組件(和子組件,這里暫未證明牲证,但確實(shí)是觸發(fā) SetSate() 后哮针,其所有子組件都將重新渲染。)
-
如何在路由轉(zhuǎn)換之間保持狀態(tài)同步坦袍?
父組件傳遞狀態(tài)值到子組件十厢,子組件拿到并顯示,但卻不能實(shí)時(shí)更改??捂齐,我一時(shí)半會(huì)還正沒想出什么解決方法蛮放,我相信即使能做到也不優(yōu)雅。
證明觸發(fā) SetSate() 后奠宜,其所有子組件都將重新渲染:我在父組件中添加兩個(gè)子組件包颁,一旦觸發(fā)渲染變打印相關(guān)數(shù)據(jù):
TestStateless(),
TestStateful()
class TestStateless extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('build TestStateless');
return Text('TestStateless');
}
}
class TestStateful extends StatefulWidget {
@override
_TestStatefulState createState() => _TestStatefulState();
}
class _TestStatefulState extends State<TestStateful> {
@override
Widget build(BuildContext context) {
print('build TestStateful');
return Text('_TestStatefulState');
}
}
此時(shí)到 PageTwo 觸發(fā) add 事件,日志出來:
通過這種簡單的方式已經(jīng)可以說明一個(gè)問題压真,即以最簡單的方式我們已經(jīng)可以完成狀態(tài)傳遞和組件渲染娩嚼,而路由間保持狀態(tài)一致還不能解決。
InheritedWidget
Google 官方給我們的解決方案是 InheritedWidget
滴肿,怎么理解他岳悟,我們可以稱它為“狀態(tài)樹”,它使得所有的 widget 的 State 來源統(tǒng)一泼差,這樣一旦有一處觸發(fā)狀態(tài)改變贵少,F(xiàn)lutter 以某種方式感應(yīng)到了(有個(gè)監(jiān)聽器)和屎,砍掉它,長出一個(gè)新樹春瞬,Perfect柴信!所有地方都能感受到他的變化。上面提到的第一種狀態(tài)管理方式 ScopedModel
便是基于此而產(chǎn)生的一套第三方庫宽气。
其實(shí)現(xiàn)在看來 InheritedWidget 已經(jīng)非常簡單了随常,我們抓住兩個(gè)點(diǎn)即可完全掌握它:
-
狀態(tài)樹中的數(shù)據(jù)
class MyInheritedValue extends InheritedWidget { const MyInheritedValue({ Key key, @required this.value, @required Widget child, }) : assert(value != null), assert(child != null), super(key: key, child: child); final int value; static MyInheritedValue of(BuildContext context) { return context.inheritFromWidgetOfExactType(MyInheritedValue); } @override bool updateShouldNotify(MyInheritedValue old) => value != old.value; }
注入到根組件中:
Widget build(BuildContext context) { return MyInheritedValue( value: 42, child: ... ); }
-
使用狀態(tài)樹中數(shù)據(jù)的其他 Widget
// 拿到狀態(tài)樹中的值 MyInheritedValue.of(context).value
請(qǐng)注意:這種情況下是不能改 InheritedWidget 中的值的,需要改也很簡單就是將 MyInheritedValue 的值封裝成一個(gè)對(duì)象萄涯,每次改變這個(gè)對(duì)象的值绪氛,具體法相看我的樣例代碼!
上面所說砍掉整棵樹過于粗暴卻并不夸張涝影,因?yàn)橐惶幐淖兯鼘⒙?lián)動(dòng)整棵樹枣察,
ScopedModel 是基于 InheritedWidget 的庫,實(shí)現(xiàn)起來與 InheritedWidget 大同小異燃逻,而且其有一種可以讓局部組件不改變的方式:設(shè)置 rebuildOnChange 為 false序目。
return ScopedModelDescendant<CartModel>(
rebuildOnChange: false,
builder: (context, child, model) => ProductSquare(
product: product,
onTap: () => model.add(product),
),
);
具體代碼請(qǐng)看 GitHub,ScopedModel 樣例截取一個(gè)老外給的實(shí)例伯襟,就是下方參考鏈接 Google 開發(fā)者大會(huì)上演講的那兩位其中之一猿涨。
這種方式顯然有點(diǎn)不足之處就是一旦遇到小規(guī)模變動(dòng)就要引起大規(guī)模重新渲染,所以當(dāng)項(xiàng)目達(dá)到一定的規(guī)哪饭郑考慮 Google 爸爸給我們的另一種解決方案叛赚。
Streams(流)
在 Android 開發(fā)中我們經(jīng)常會(huì)用到 RxJava 這類響應(yīng)式編程方法的框架,其強(qiáng)大之處無須多言稽揭,而 Stream 看上去就是在 Dart 語言中的響應(yīng)式編程的一種實(shí)現(xiàn)俺附。
-
Streams 是什么鬼?
如果要具體把 Streams 說清楚溪掀,一篇文章絕對(duì)不夠事镣,這里先介紹一下其中的概念,這篇文章目的就是如此膨桥。待我后續(xù)想好怎么具體描述清楚蛮浑。
你可以把它想象成一個(gè)管道唠叛,有入口(StreamSink)和出口()只嚣,我們將想要處理的數(shù)據(jù)從入口放入經(jīng)過該管道經(jīng)過一系列處理(經(jīng)由 StreamController)從出口中出來,而出口又有一個(gè)類似監(jiān)聽器之物艺沼,我們不知道它何時(shí)到來或者何時(shí)處理結(jié)束册舞。但是當(dāng)出口的監(jiān)聽器拿到東西便立即做出相應(yīng)的反應(yīng)。
哪些東西可以放入管道障般?
任何變量调鲸、對(duì)象盛杰、數(shù)組、甚至事件都可以被當(dāng)作數(shù)據(jù)源從入口放進(jìn)去藐石。-
Streams 種類
- Single-subscription Stream即供,“單訂閱”流,這種類型的流只允許在該流的整個(gè)生命周期內(nèi)使用單個(gè)偵聽器于微。即使在第一個(gè)訂閱被取消后逗嫡,也無法在此類流上收聽兩次。
- Broadcast Streams株依,第二種類型的 Stream 允許任意數(shù)量的偵聽器驱证。可以隨時(shí)向廣播流添加偵聽器恋腕。 新的偵聽器將在它開始收聽 Stream 時(shí)收到事件抹锄。
例子:
第一個(gè)示例描述了“單訂閱”流,只打印輸入的數(shù)據(jù)荠藤。 你會(huì)發(fā)現(xiàn)是哪種數(shù)據(jù)類型無關(guān)緊要伙单。
import 'dart:async';
void main() {
//
// Initialize a "Single-Subscription" Stream controller
//
final StreamController ctrl = StreamController();
//
// Initialize a single listener which simply prints the data
// as soon as it receives it
//
final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));
//
// We here add the data that will flow inside the stream
//
ctrl.sink.add('my name');
ctrl.sink.add(1234);
ctrl.sink.add({'a': 'element A', 'b': 'element B'});
ctrl.sink.add(123.45);
//
// We release the StreamController
//
ctrl.close();
}
第二個(gè)示例描述了“廣播”流,它傳達(dá)整數(shù)值并僅打印偶數(shù)哈肖。 我們用 StreamTransformer 來過濾(第14行)值车份,只讓偶數(shù)經(jīng)過。
import 'dart:async';
void main() {
//
// Initialize a "Broadcast" Stream controller of integers
//
final StreamController<int> ctrl = StreamController<int>.broadcast();
//
// Initialize a single listener which filters out the odd numbers and
// only prints the even numbers
//
final StreamSubscription subscription = ctrl.stream
.where((value) => (value % 2 == 0))
.listen((value) => print('$value'));
//
// We here add the data that will flow inside the stream
//
for(int i=1; i<11; i++){
ctrl.sink.add(i);
}
//
// We release the StreamController
//
ctrl.close();
}
RxDart
RxDart包是 ReactiveX API 的 Dart 實(shí)現(xiàn)牡彻,它擴(kuò)展了原始的 Dart Streams API 以符合 ReactiveX 標(biāo)準(zhǔn)扫沼。
由于它最初并未由 Google 定義,因此它使用不同于 Dart 的變量庄吼。 下表給出了 Dart 和 RxDart 之間的關(guān)系缎除。
Dart | RxDart |
---|---|
Stream | Observable |
StreamController | Subject |
RxDart 擴(kuò)展了原始的 Dart Streams API 并提供了 StreamController 的3個(gè)主要變體:
-
PublishSubject
PublishSubject 是一個(gè)普通的 broadcast StreamController ,有一點(diǎn)不同:stream 返回一個(gè) Observable 而不是一個(gè) Stream 总寻。
如您所見器罐,PublishSubject 僅向偵聽器發(fā)送在訂閱之后添加到 Stream 的事件。
-
BehaviorSubject
BehaviorSubject 也是一個(gè) broadcast StreamController渐行,它返回一個(gè) Observable 而不是一個(gè)Stream轰坊。
與 PublishSubject 的主要區(qū)別在于 BehaviorSubject 還將最后發(fā)送的事件發(fā)送給剛剛訂閱的偵聽器。
-
ReplaySubject
ReplaySubject 也是一個(gè)廣播 StreamController祟印,它返回一個(gè) Observable 而不是一個(gè) Stream肴沫。(蘿莉啰嗦)
默認(rèn)情況下,ReplaySubject 將Stream 已經(jīng)發(fā)出的所有事件作為第一個(gè)事件發(fā)送到任何新的偵聽器蕴忆。
BloC
BLoC 代表業(yè)務(wù)邏輯組件 (Business Logic Component)颤芬。一般的 Flutter 代碼業(yè)務(wù)邏輯和UI組件糅合在一起,不方便測(cè)試,不利于單獨(dú)的測(cè)試業(yè)務(wù)邏輯部分站蝠,不能更好的重用業(yè)務(wù)邏輯代碼汰具,體現(xiàn)在,如果網(wǎng)絡(luò)請(qǐng)求的邏輯有所變動(dòng)的話菱魔,加入這個(gè)業(yè)務(wù)功能被兩個(gè)端(web留荔、flutter)使用的話,是需要改動(dòng)兩個(gè)地方的澜倦。
簡而言之存谎,業(yè)務(wù)邏輯需要:
- 被移植到一個(gè)或幾個(gè) BLoC 中,
- 盡可能從表示層中刪除肥隆。 也就是說既荚,UI組件應(yīng)該只關(guān)心UI事物而不關(guān)心業(yè)務(wù),
- 依賴 Streams 使用輸入(Sink)和輸出(stream)栋艳,
- 保持平臺(tái)獨(dú)立恰聘,
- 保持環(huán)境獨(dú)立。
事實(shí)上吸占,BLoC 模式最初的設(shè)想是實(shí)現(xiàn)允許獨(dú)立于平臺(tái)重用相同的代碼:Web應(yīng)用程序晴叨,移動(dòng)應(yīng)用程序,后端矾屯。
Bloc 的大概就是 Stream 在 Flutter 中的最佳實(shí)踐:
- 組件通過 Sinks 向 BLoC 發(fā)送事件兼蕊,
- BLoC 通過 stream 通知組件,
- 由 BLoC 實(shí)現(xiàn)的業(yè)務(wù)邏輯件蚕。
將 BloC 應(yīng)用在計(jì)數(shù)器應(yīng)用中:
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Streams Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: BlocProvider<IncrementBloc>(
bloc: IncrementBloc(),
child: CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Stream version of the Counter App')),
body: Center(
child: StreamBuilder<int>(
stream: bloc.outCounter,
initialData: 0,
builder: (BuildContext context, AsyncSnapshot<int> snapshot){
return Text('You hit me: ${snapshot.data} times');
}
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: (){
bloc.incrementCounter.add(null);
},
),
);
}
}
class IncrementBloc implements BlocBase {
int _counter;
//
// Stream to handle the counter
//
StreamController<int> _counterController = StreamController<int>();
StreamSink<int> get _inAdd => _counterController.sink;
Stream<int> get outCounter => _counterController.stream;
//
// Stream to handle the action on the counter
//
StreamController _actionController = StreamController();
StreamSink get incrementCounter => _actionController.sink;
//
// Constructor
//
IncrementBloc(){
_counter = 0;
_actionController.stream
.listen(_handleLogic);
}
void dispose(){
_actionController.close();
_counterController.close();
}
void _handleLogic(data){
_counter = _counter + 1;
_inAdd.add(_counter);
}
}
你一定在說孙技,臥槽,哇靠~~什么吊玩意排作,那么就留著懸念吧牵啦,今天寫不動(dòng)了!
Bolc 的具體實(shí)現(xiàn)我在樣例代碼里分兩步走放在兩個(gè)文件夾里妄痪!如果需要可以先去看看嘗嘗鮮哈雏。
這篇文章的目的就是介紹一些概念給大家關(guān)于 Streams、RXDart 及 Bloc 詳細(xì)明了的解釋后續(xù)更新衫生!
樣例代碼
https://github.com/MeandNi/Flutter_StatePro