做者:閒魚技術-匠修
Flutter Widget的設計靈感來源於React,是一款原生就立足於響應式的UI框架。本文基於Flutter特色,試圖結合閒魚在Flutter的工程應用來談下咱們對Flutter React編程範式的思考和踐行。前端
談起UI總會講到MVC,它出現的時間很早,那時候尚未普及現代GUI普遍使用的事件驅動(消息循環)模型,因此很長的時間內,MVC都在進化,不斷的被從新定義。到如今MVC已是一個很寬泛的概念了。使用基礎的MVC做爲框架來開發容易出現模塊職責邊界模糊,邏輯調用方向混亂。GUI框架進化後,將用戶事件的分發處理集成到了View模塊中,由此出現了MVP,MVP職責劃分較清晰,邏輯調用方向也比較好把握,可是很繁瑣,開發效率不高。再隨着Web的發展,標記語言被應用於界面描述,開始出現邏輯界面分離和無狀態化界面,MVVM應運而生。MVVM讓架構層面來提供數據和View的雙向綁定,減輕了開發工做,但有時候也帶來了必定程度的狀態混亂。函數式編程在近年被從新提起,並引起潮流,催生了響應式界面開發,響應式是對GUI事件驅動模型的一種返璞歸真。react
從迭代歷程上看,Model和View是兩個相對固定的角色,它們容易理解,也能很好的肯定職責邊界。如何去溝通Model和View是架構設計的關鍵,響應式的通常作法是讓Model回到最初的事件驅動,結合函數式的數據流來驅動View刷新。這樣有比較清晰的角色劃分和簡單易於理解的邏輯連接,能較好的統一編程模式。git
一般GUI框架都有一些共同點,好比View的樹形層級,消息循環,Vsync信號刷新等,Flutter也繼承這些經典的設計,可是Flutter並無使用標記語言來描述界面(例如Web中的HTML,Android中的XML),這其中有Flutter立足於響應式的初衷。Reactive是一款將事件數據流做爲核心的開發模型,UI框架會提供相應的特性來提供更好的支持。github
有一種說法認爲函數式語言和命令式語言的不一樣在於命令式語言是給計算機下達指令而函數式語言是向計算機描述邏輯。這種思路在Flutter UI中獲得了體現。Flutter不提倡去操做UI,它固然也基本不會提供操做View的API,好比咱們常見的相似TextView.setText(),Button.setOnClick()這種是不會有的。對界面的描述是能夠數據化的(相似XML,JSON等),而對界面的操做是很難數據化的,這很重要,響應式須要方即可持續的將數據映射成界面。
在Flutter中用Widget來描述界面,Widget只是View的「配置信息」,編寫的時候利用Dart語言一些聲明式特性來獲得相似結構化標記語言的可讀性。不論Stateless Widget 仍是 Stateful Widget都是不可變的(immutable),其中的成員變量也應該都是final的,也就是說,Widget是「只讀」的。Widget是數據的映射,當數據改變的時候,咱們須要從新建立Widget去更新界面,這意味着Widget會建立銷燬的很是頻繁,不過Flutter使用的Dart虛擬機能高效的處理這種短週期的輕量對象。編程
這種設計思路對剛接觸的開發者可能有些不習慣,咱們能夠藉助開發Android中的ListView(iOS中的TableView)來理解:咱們一般先準備好一個數據List,而後實現一個Adapter來將List中的items映射成一個個itemView,最後將List和Adapter設置給ListView。這樣當咱們改變List中的數據,ListView就會相應的刷新View。Flutter相似,咱們準備好Widgets(只不過Widget的「容器」是Tree而不是List),Flutter會提供Adapter(RenderObjectToWidgetAdapter)將其映射成渲染用的RenderObject,當Widget更新時就會刷新界面。
redux
另外,Widget也能經過設置Key來緩存複用,在相似ListView的場景中,Item Widget的複用是頗有收益的。api
在咱們國家,若是你想和別人溝通上拉近距離,有時候會進入到相似「咱們500年前是一家」的這種語境中。在Flutter中,若是兩個組件要通訊,也是去找祖先(固然,也有可能兩個組件自己就有遺傳關係),Flutter把它描述成「數據上行,通知下行」。緩存
可是,在一個很是複雜的樹形層級中,要找到某位「祖先」並非很容易的事情,並且性能也很差。Flutter爲此作了優化,提供了InheritedWidget,「祖先」Widget繼承該類型後,child能夠經過BuildContext中提供的inheritFromWidgetOfExactType方法方便的找到在層級中離的最近的那位「祖先」。該方法作了優化,效率很高,而且可讓child和「祖先」創建依賴關係,方便作刷新。bash
Flutter中並無提倡相似controller的概念(像Android中的Activity,iOS中的ViewController),自己View是不可操做的,controller也就失去了意義。那麼,組件之間的通訊就必須在View層「自力更生」了。網絡
這確定不是Flutter纔有的,要想把響應式實現的簡潔優雅,就要利用好語言的函數式特性。Flutter的亮點是它使用的Dart語言能把這件事情變的很輕量,你基本不須要引入什麼第三方庫就能作到(不過確實有RxDart庫,但感受只是作了額外的加強),並且明顯語言Api的設計也往這個方向上作了優化,很是方便。具體能夠看看Stream和RxDart。
經過React的實踐,響應式能夠很好的解決數據到界面的更新,並且效率也不錯。可是自身對數據狀態的管理不足,React官方提出了Flux,而在面對複雜業務場景時,Flutter官方也是推薦Redux架構,咱們也是根據這一思路搭建的框架。
首先是業務邏輯和界面分離,界面是無狀態(Stateless)的,咱們也正在嘗試自動化的方法直接生成界面代碼,因此Widget中是不會有業務邏輯代碼的。當咱們把一個能描述當前界面的數據(State)交給View層時,界面就應該能正常展現。用戶和界面交互會產生Action,Action表明了用戶交互的意圖,Action能夠攜帶信息(好比用戶使用輸入留言,Action中就應該攜帶用戶留言的內容信息)。Action會輸入給Store,Store會經過註冊的Interrupters對Action作前期攔截處理,能夠經過Interrupter截攔Action,也能夠把一個Action從新改寫成另外的Action。Store而後收集相應綁定的Reducers對Action作一次reduce操做,產生新的State,並通知界面刷新。
一般咱們在建立Store的時候就組冊好Reducer和Interrupter:
Store<PublishState> buildPublishStore(String itemId) {
//設置狀態初始值
PublishState initState = new PublishState();
initState.itemId = itemId;
initState.isLoading = true;
//建立Reducer和對應Action的綁定
var reducerBinder = ActionBinder.reducerBinder<PublishState>()
..bind(PublishAction.DETAIL_LOAD_COMPLETED, _loadCompletedReducer)
..bind(PublishAction.DELETE_IMAGE, _delImageReducer)
..bind(PublishAction.ADD_IMAGE, _addImageReducer);
//建立Interrupter和對應Action的綁定
var interrupterBinder = ActionBinder.interrupterBinder<PublishState>()
..bind(PublishAction.LOAD_DETAIL, _loadDataInterrupter)
..bind(PublishAction.ADD_IMAGE, UploadInterruper.imageUploadInterrupter);
//建立Store
return new CommonStore<PublishState>(
name: 'Publish',
initValue: initState,
reducer: reducerBinder,
interrupter: interrupterBinder);
}複製代碼
Reducer中就是處理用戶交互時產生的Action的邏輯代碼,接收3個參數,一個是執行上下文,一個要處理的Action,一個是當前的State,處理結束後必須返回新的State。函數式理想的Reducer應該是一個無反作用的純函數,顯然咱們不該該在Reducer中去訪問或者改變全局域的變量,但有時候咱們會對前面的計算結果有依賴,這時能夠將一些運行時數據寄存在ReduceContext中。Reducer中不該該有異步邏輯,由於Store作Reduce操做是同步的,產生新State後會當即通知界面刷新,而異步產生對State的更新並不會觸發刷新。
PublishState _delImageReducer(ReduceContext<PublishState> ctx, Action action, PublishState state) {
int index = action.args.deleteId;
state.imageUplads.removeAt(index);
return state;
}複製代碼
Interrupter形式上和Reducer相似,不一樣的是裏面能夠作異步的邏輯處理,好比網絡請求就應該放在Interrupter中實現。
///截攔一個網絡請求的Action,並在執行請求網絡後發出新Action
bool _onMtopReq(InterrupterContext<S> ctx, Action action) {
NetService.requestLight(
api: action.args.api,
version: action.args.ver,
params: action.args.params,
success: (data) {
ctx.store.dispatch(Action.obtain(Common.MTOP_RESPONSE)
..args.mtopResult = 'success'
..args.data = data);
},
failed: (code, msg) {
ctx.store.dispatch(Action.obtain(Common.MTOP_RESPONSE)
..args.mtopResult = 'failed'
..args.code = code
..args.msg = msg);
});
return true;
}複製代碼
一般咱們會讓一個界面根部的InheritedWidget來持有Store,這樣界面上的任何Widget
都能方便的訪問到Store,並和Store創建聯繫。這種作法能夠參考redux_demo,再此不詳細展開。
最後簡單的說說Store的實現,Store可以接收Action,而後執行reduce,最後向widget提供數據源。Widget能夠基於提供的數據源創建數據流,響應數據變動來刷新界面。這其中最核心的就是Dart的Stream。
......
//建立分發數據的Stream
_changeController = new StreamController.broadcast(sync: false);
//建立接收Action的Stream
_dispatchController = new StreamController.broadcast(sync: false);
//設置響應Action的函數
_dispatchController.stream.listen((action) {
_handleAction(action);
});
......
//向Store中分發Action
void dispatch(Action action) {
_dispatchController.add(action);
}
//Store向外提供的數據源
Stream<State> get onChange => _changeController.stream;複製代碼
Store中最核心的對Action進行reduce操做:
//收集該Action綁定的Reducer
final List<ReduceContext<State>> reducers = _reducers.values
.where((ctx) => ctx._handleWhats.any((what) => what == action.what))
.toList();
//執行reduce
Box<Action, State> box = new Box<Action, State>(action, _state);
box = reducers.fold(box, (box, reducer) {
box.state = reducer._onReduce(box.action, box.state);
return box;
});
//觸發更新
_state = box.state;
_changeController.add(_state); 複製代碼
Widget基於Store暴露的數據源創建數據流:
store.onChange
//將Store中的數據轉換成Widget須要的數據
.map((state) => widget.converter(state))
//比較前一次數據,若是想等則不用更新界面
.where((value) => (value != latestValue))
//更新界面
.listen((value){
...
setState()
...
})複製代碼
咱們在業務開發中發現,有時候一個頁面一個Store會帶來組件複用上的不方便,好比視頻播放組件是一個邏輯比較內聚的組件,若是把它的reducer都集中放在頁面的Store中那麼別的頁面想要複用這個開發好的視頻組件就不方便了,這時候視頻組件可能須要一個獨立的Store來存放視頻播放相關的邏輯。咱們遵循Flutter組件通訊方法,將框架擴展爲容許存在多個Store,而且作到對Widget開發無感知。
Widget只能感知離它最近的Store持有者,該Store會向更高層級Store轉發Action,同時接收來自更高層級Store的數據變動並通知Widget。
相對目前流行的MVVM框架(Vue,Angular)可以細粒度的綁定數據,並實現界面的最小化刷新,Flutter上面尚未找到很好的辦法可以在框架內自動實現,目前只能依賴開發者去手動處理。這難免會下降開發效率,拉低開發體驗,咱們也在探索更好的方法,若是感興趣或者有好的解決思路,歡迎和咱們交流。
當遇到狀態複雜頁面(多動畫,多view聯動)時,Store中應該要提供相關工具或機制來管理複雜的狀態來提升開發效率,狀態機是個可選的方案之一。若是有在Dart下優雅的狀態機框架實現或思路,請務必和咱們分享一下。
最後,閒魚技術團隊廣招各種方向的達人,不管你是精通移動端,前端,後臺,仍是機器學習,音視頻,自動化測試等,都歡迎投遞簡歷加入咱們,一同用技術改善生活!
簡歷投遞:guicai.gxy@alibaba-inc.com
本文爲雲棲社區原創內容,未經容許不得轉載