告別setState()! 優雅的UI與Model綁定 Flutter DataBus使用~

Flutter開發中,你們都繞不開Widget的刷新,setState()是最簡單的用法。但隨着當app的交互變得複雜,setState出現的次數便會顯著增長,每次setState都會從新調用build方法,這勢必對於性能以及代碼的可閱讀性帶來必定的影響。如何優雅的解決這個問題,不得不提到StreamBuilder,StreamBuilder是Flutter中異步構建的核心組件。許多著名的開源框架例如Bloc皆是基於此實現。git

若是StreamBuilder有了解能夠直接看第二部分github

git連接:github.com/canoninmajo…編程

1、局部刷新的關鍵點 StreamBuilder

  • setState()

如今頁面上有兩個數字key1和key2須要展現,當點擊上方的按鈕時,咱們對應修改key1或者key2的值。 採用setState()的方式,咱們知道很簡單,創建本地變量key1,key2,而後放入對應的Text中直接展現。當咱們點擊按鈕時使本地變量key1,key2作增長操做,以後調用setState()。markdown

但當我刷新Key1的時候, 會同時重構Key2展現的兩個Text,即便個人key2沒有發生變化,顯然這不是一種合理的作法。app

其實Flutter中還提供了一個強大組件SteamBuilder來協助咱們處理控件的刷新構建。框架


  • StreamBuilder

如圖,是StreamBuilder使用基本結構,StreamBuidler基於dart中的異步核心之一Stream,採起觀察者模式,發送方經過StreamControll發送數據,觀察對象接收到數據後構建本身的內容。異步

從代碼可知StreamBuilder接受兩個參數,一個stream,表示咱們監聽的Stream(一個StreamBuilder監聽一個Stream,可是一個Stream能被多個Widget監聽),builder中傳入咱們須要構建的contentWidget。ide

這樣Widget的構建徹底由Stream觸發,控件無需自行setState,它的構建徹底由數據驅動,是一種響應式編程。也是許多開源框架例如Bloc等核心原理。oop


回到上面的例子中,當咱們採用StreamBuilder後,上面的例子就變得很是的清晰了,咱們創建兩條StreamControler,而後把圖中的展現key1和key2的兩組Text分別由兩個StreamBuilder包裹。 在key1的點擊事件中往Stream中add數據,這樣在key1的流上產生了一條數據,對應的監聽者收到數據後,只更新本身的內容,不會重建其餘區域。 性能


2、DataLine如何優化StreamBuilder的麻煩使用

通過上面的瞭解,咱們知道。StreamBuilder能夠完美解決局部刷新的問題,但StreamBuilder也有着一樣明顯的缺點,使用起來很是麻煩,須要本身手動建立流,將控件用StreamBuilder包裹構造。

當咱們的頁面須要多個局部刷新的時候,Stream的編寫將會很是麻煩。相似Provide的解決方案也須要設定頂級Widget,而後用consumer包裹子控件,調用更新等等操做。

有沒有什麼方式能夠簡化咱們的使用呢?

咱們注意到,StreamBuilder須要監聽一個stream,而這個stream每每來自StreamControler。對於每一個StreamControler來講,就像生活中的一條 一對多的數據線數據線(DataLine)同樣。 對於這條DataLine,最核心的有兩個方法

一、添加觀察者(經過StreamBuilder包裹實際展現的contentWidget) : 相似數據線鏈接手機

二、發送數據 :相似經過數據線給手機充電(由於是一對多的過程)

基於這種思路,設計了一個SingleDataLine,對於這條「數據線"而言,其中T約束了這條線的使用數據類型,currentData能幫助咱們拿到當前最新的數據,setData(T t)發送數據。

核心在於咱們的addObserver中,該方法須要傳入一個 返回值爲Widget Function(BuildContext context, T data) observer的方法,這個傳入的方法正是咱們須要構建的Widget的構造方法。

class SingleDataLine<T> {
  StreamController<T> _stream;

  //拿到當前最新的數據
  T currentData;

  SingleDataLine([T initData]) {
    currentData = initData;
    _stream = initData == null
        ? BehaviorSubject<T>()
        : BehaviorSubject<T>.seeded(initData);
  }

  get outer => _stream.stream;

  get inner => _stream.sink;

  void setData(T t) {
    //同值過濾
    if (t == currentData) return;
    //防止關閉
    if (_stream.isClosed) return;
    currentData = t;
    inner.add(t);
  }

  Widget addObserver(
    Widget Function(BuildContext context, T data) observer,
  ) {
    return DataObserverWidget<T>(this, observer);
  }

  void dispose() {
    _stream.close();
  }
}
複製代碼

這個addObserver方法返回一個DataObserverWidget控件,這個組件就是幫咱們對StreamBuilder進行了封裝,以此簡化StreamBuilder的使用。

class _DataObserverWidgetState<T> extends State<DataObserverWidget<T>> {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return StreamBuilder(
      stream: widget._dataLine.outer,
      builder: (context, AsyncSnapshot<T> snapshot) {
        if (snapshot != null && snapshot.data != null) {
          print(
              " ${context.widget.toString()} 中的steam接收到了一次數據${snapshot.data}");
          return widget._builder(context, snapshot.data);
        } else {
          return Row();
        }
      },
    );
  }

  @override
  void dispose() {
    super.dispose();
    widget._dataLine.dispose();
  }
複製代碼

3、DataBus如何解決多個Stream的綁定

上面咱們經過SingDataLine簡化了StreamBuilder的使用,但當頁面中有多個SingleDataLine的時候,對它的建立和管理,可能會成爲一件麻煩的事兒。基於此設計了一個dataBus總線管理。 咱們將每個key和對應的DataLine存入Map中進行管理,經過直接調用getLine(key)的方法獲取建立DataLine。並且因爲MultDataLine是mixin定義,因此咱們能夠在任意的類中混入使用方法。例如直接在Widget中混入改類,調用getLine方法獲取到StreamBuilder。

import 'package:flutter_dpluse_package/common/widgets/Page/data_line.dart';
mixin MultDataLine {
  final Map<String, SingleDataLine> dataBus = Map();

  SingleDataLine<T> getLine<T>(String key) {
    if (!dataBus.containsKey(key)) {
      SingleDataLine<T> dataLine = new SingleDataLine<T>();
      dataBus[key] = dataLine;
    }
    return dataBus[key];
  }

  void dispose() {
    dataBus.values.forEach((f) => f.dispose());
    dataBus.clear();
  }
}
複製代碼

回到上面的例子,使用DataBus,頁面的構建將會極其簡單,其中核心的發送數據和監聽咱們經過getLine實現。

ListView(children: <Widget>[
      GestureDetector(
        child: Container(
          width: 150,
          height: 60,
          child: Center(
              child: Text(
            'key1的觸發者',
            style: TextStyle(color: Colors.white, fontSize: 20),
          )),
          decoration: BoxDecoration(color: Colors.grey),
        ),
        onTap: () {
            //發送一個數據
          getLine(KEY1).setData(key1++);
        },
      ),
        //綁定監聽對象
      getLine(KEY1).addObserver((context, data) {
        //實際的觀察者
        return Text(
          'key1當前的數據爲 $data',
          style: TextStyle(
              fontSize: 19, color: Colors.green, fontWeight: FontWeight.w600),
        );
      }),
      getLine(KEY1).addObserver(
        (context, data) {
          return Text(
            'key1當前的數據爲 $data',
            style: TextStyle(
                fontSize: 19, color: Colors.blue, fontWeight: FontWeight.w600),
          );
        },
      ),]);
複製代碼

4、總結

DataBus中使用了StreamBuilder做爲構建方式,其實系統中還有一些輕量的觀察模式組件可供選擇,例如ChangNotify等,但若是單獨使用這些組件不可避免觀察對象散落在頁面中的各個位置,不易於管理。DataBus是我的在開發中實踐出一種極簡的UI與Model的綁定方法,基於此實現一套普通頁面框架,已實踐過多個複雜頁面。 DataBus核心想解決兩個問題:一、簡化觀察對象與被觀察者的綁定 二、統一的管理全部綁定關係的生命週期 github.com/canoninmajo…

相關文章
相關標籤/搜索