閒魚專家詳解:Flutter React程式設計正規化實踐

HitTwice發表於2018-07-21

作者:閒魚技術-匠修

Flutter Widget的設計靈感來源於React,是一款原生就立足於響應式的UI框架。本文基於Flutter特點,試圖結合閒魚在Flutter的工程應用來談下我們對Flutter React程式設計正規化的思考和踐行。

Reactive的誕生

談起UI總會講到MVC,它出現的時間很早,那時候還沒有普及現代GUI廣泛使用的事件驅動(訊息迴圈)模型,所以很長的時間內,MVC都在進化,不斷的被重新定義。到現在MVC已經是一個很寬泛的概念了。使用基礎的MVC作為框架來開發容易出現模組職責邊界模糊,邏輯呼叫方向混亂。GUI框架進化後,將使用者事件的分發處理整合到了View模組中,由此出現了MVP,MVP職責劃分較清晰,邏輯呼叫方向也比較好把握,但是很繁瑣,開發效率不高。再隨著Web的發展,標記語言被應用於介面描述,開始出現邏輯介面分離和無狀態化介面,MVVM應運而生。MVVM讓架構層面來提供資料和View的雙向繫結,減輕了開發工作,但有時候也帶來了一定程度的狀態混亂。函數語言程式設計在近年被重新提起,並引發潮流,催生了響應式介面開發,響應式是對GUI事件驅動模型的一種返璞歸真。

個人對前端架構迭代的理解:

從迭代歷程上看,Model和View是兩個相對固定的角色,它們容易理解,也能很好的確定職責邊界。如何去溝通Model和View是架構設計的關鍵,響應式的一般做法是讓Model回到最初的事件驅動,結合函式式的資料流來驅動View重新整理。這樣有比較清晰的角色劃分和簡單易於理解的邏輯連結,能較好的統一程式設計模式。

Flutter的Reactive特性

通常GUI框架都有一些共同點,比如View的樹形層級,訊息迴圈,Vsync訊號重新整理等,Flutter也繼承這些經典的設計,但是Flutter並沒有使用標記語言來描述介面(例如Web中的HTML,Android中的XML),這其中有Flutter立足於響應式的初衷。Reactive是一款將事件資料流作為核心的開發模型,UI框架會提供相應的特性來提供更好的支援。

1.描述介面而不要操作介面

有一種說法認為函式式語言和命令式語言的不同在於命令式語言是給計算機下達指令而函式式語言是向計算機描述邏輯。這種思路在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更新時就會重新整理介面。

另外,Widget也能通過設定Key來快取複用,在類似ListView的場景中,Item Widget的複用是很有收益的。

2.基於共同祖先通訊

在我們國家,如果你想和別人溝通上拉近距離,有時候會進入到類似“我們500年前是一家”的這種語境中。在Flutter中,如果兩個元件要通訊,也是去找祖先(當然,也有可能兩個元件本身就有遺傳關係),Flutter把它描述成“資料上行,通知下行”。

但是,在一個非常複雜的樹形層級中,要找到某位“祖先”並不是很容易的事情,而且效能也不好。Flutter為此做了優化,提供了InheritedWidget,“祖先”Widget繼承該型別後,child可以通過BuildContext中提供的inheritFromWidgetOfExactType方法方便的找到在層級中離的最近的那位“祖先”。該方法做了優化,效率很高,並且可以讓child和“祖先”建立依賴關係,方便做重新整理。

Flutter中並沒有提倡類似controller的概念(像Android中的Activity,iOS中的ViewController),本身View是不可操作的,controller也就失去了意義。那麼,元件之間的通訊就必須在View層“自力更生”了。

3.函式式資料流

這肯定不是Flutter才有的,要想把響應式實現的簡潔優雅,就要利用好語言的函式式特性。Flutter的亮點是它使用的Dart語言能把這件事情變的很輕量,你基本不需要引入什麼第三方庫就能做到(不過確實有RxDart庫,但感覺只是做了額外的增強),而且明顯語言Api的設計也往這個方向上做了優化,非常方便。具體可以看看Stream和RxDart。

基於React的框架實踐

統一狀態管理和單向資料流

通過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中實現。

*為什麼會有Interrupter呢?換一個角度,我們可以把整個Store看成一個函式,輸入是Action,輸出的是State。函式會有副作用,有時我們輸入引數並不一定得會相應有輸出,比如日誌函式( void log(String) ),我們輸入String只會在標準輸出上列印一個字串,log函式不會有返回值。同樣,對Store來說,也不是所有的Action都要去改變State,使用者有時候觸發Action只要想讓手機震動下而已,並不會觸發介面更新。所以,Interrupter就是Store用來處理副作用的。

  ///截攔一個網路請求的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

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31473948/viewspace-2158282/,如需轉載,請註明出處,否則將追究法律責任。

相關文章