(十)Flutter FutureBuilder 優雅構建非同步UI

Chiclaim發表於2019-08-25

前言

在實際開發中, 一般在展示列表內容之前需要先展示一個 loading 表示正在載入, 當載入成功後展示列表內容, 載入失敗展示失敗的介面

所以, 這樣一個需求就涉及到了三種情況:

  • 載入中
  • 載入成功展示列表
  • 載入失敗展示錯誤

從前面的文章(《(二)Flutter 學習之 Dart 展開操作符和 Control Flow Collections》)[chiclaim.blog.csdn.net/article/det…] 我們知道 Flutter UI宣告式UI

不同UI的切換時通過 setState 來重新構建的. 那麼上面的三種情況UI 我們需要通過 if else 來判斷到底展示那種介面.

例如下面的虛擬碼:

@override
Widget build(BuildContext context) {
    if(loading) { // 正在載入
      return Text("Loading...");
    } else if(isError) { // 載入出錯
      return Text("Error...");
    } else {   // 展示列表內容
      return ListView(...)
    }
}
複製程式碼

這種方式雖然也能實現上面的需求, 但是不利於程式碼的維護, 需要維護很多變數, 很不優雅.

FutureBuilder

FutureBuilder 的用法很簡單, 主要涉及兩個引數:

  • future 指定非同步任務, 交給 FutureBuilder 管理
  • builder 根據非同步任務的狀態來構建不同的 Widget, 類似上面的 if/else

FutureBuilder 中的非同步任務狀態有:

狀態 描述
none 沒有連線到任何非同步任務
waiting 已連線到非同步任務等待被互動
active 已連線到一個已啟用的非同步任務
done 已連線到一個已結束的非同步任務

我們可以使用 FutureBuilder 改造上面的案例, 程式碼如下所示:

FutureBuilder<int>(
    future: _loadList(),
    builder: (context, snapshot) {
      switch (snapshot.connectionState) {
        case ConnectionState.none:
        case ConnectionState.waiting:
        case ConnectionState.active:
          // 顯示正在載入
          return createLoadingWidget();
        case ConnectionState.done:
          // 提示錯誤資訊
          if (snapshot.hasError) {
            return createErrorWidget(snapshot.error.toString());
          }
          // 展示列表內容
          return ListView.separated(
            itemCount: snapshot.data,
            itemBuilder: (BuildContext context, int index) {
              return ListTile(title: Text(index.toString()));
            },
            separatorBuilder: (BuildContext context, int index) {
              return divider;
            },
          );
        default:
          return Text("unknown state");
)
複製程式碼

需要注意的是, 上面的程式碼介面每次被重建的時候都會執行 loadList 操作.

但是有的時候並不是介面發生變化的時候都需要去重新執行 future, 例如介面一個 Tab + ListView(文章分類+文章列表), 文章分類是需要先載入, 那麼文章分類的非同步任務就是 future, 載入成功分類後, 才能去載入文章列表, 列表載入成功介面會重新構建, 這個時候是不應該再次載入文章分類的(future)

這個時候需要在把 future 變數作為成員變數, 在 initState 中初始化, 然後再傳遞給 future 引數, 如:

Future _future;

@override
void initState() {
    _future = _loadList();
    super.initState();
}

FutureBuilder<int>(
    future: _future,
    ...
)
複製程式碼

執行效果如下圖所示:

Flutter-BuilderFuture

FutureBuilder 原始碼分析

FutureBuilder 繼承了 StatefulWidget, 所以主要程式碼都集中在 State

class _FutureBuilderState<T> extends State<FutureBuilder<T>> {
  Object _activeCallbackIdentity;
  AsyncSnapshot<T> _snapshot;

  @override
  void initState() {
    super.initState();
    // 初始化非同步快照, 初始狀態為 none
    _snapshot = AsyncSnapshot<T>.withData(ConnectionState.none, widget.initialData);
    // 關聯非同步任務
    _subscribe();
  }

  // 頁面發生變化判斷老的widget的 future 和新widget future 是否是同一個物件
  // 如果是同一個物件則不會執行非同步任務, 否則會重新執行非同步任務
  @override
  void didUpdateWidget(FutureBuilder<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.future != widget.future) {
      if (_activeCallbackIdentity != null) {
        _unsubscribe();
        _snapshot = _snapshot.inState(ConnectionState.none);
      }
      _subscribe();
    }
  }

  // 執行外部傳入的 builder 回撥
  // widget 就是 State 對應的 FutureBuilder(StatefulWidget)
  @override
  Widget build(BuildContext context) => widget.builder(context, _snapshot);

  @override
  void dispose() {
    _unsubscribe();
    super.dispose();
  }

  void _subscribe() {
    if (widget.future != null) {
      final Object callbackIdentity = Object();
      _activeCallbackIdentity = callbackIdentity;
      // 開始執行非同步任務
      widget.future.then<void>((T data) {
        if (_activeCallbackIdentity == callbackIdentity) {
          // 重新整理介面
          setState(() {
            // 元件非同步快照資料
            _snapshot = AsyncSnapshot<T>.withData(ConnectionState.done, data);
          });
        }
      }, onError: (Object error) {
        // 執行非同步任務發生異常
        if (_activeCallbackIdentity == callbackIdentity) {
          setState(() {
            _snapshot = AsyncSnapshot<T>.withError(ConnectionState.done, error);
          });
        }
      });
      
      // 將非同步任務狀態設定為 waiting
      _snapshot = _snapshot.inState(ConnectionState.waiting);
    }
  }

  void _unsubscribe() {
    _activeCallbackIdentity = null;
  }
複製程式碼

StreamBuilder

除了 FutureBuilder 可以優雅構建非同步UI, StreamBuilder 也可以實現, 但是一般的非同步任務 UI 展示並不是一個 Stream 流的形式, 更像是一次性的邏輯處理, 只要成功後, 一般不需要更新, 所以使用 FutureBuilder 就完全夠了. 實際開發中根據情況來選擇. StreamBuilder 的功能更加強大, 後期如果往 stream 中傳送資料 UI 介面也跟著發生變化 如:

StreamBuilder<int>(
  // 這個是stream 而不是 future
  stream: _streamController.stream,
  initialData: _counter,
  builder: (BuildContext context, AsyncSnapshot<int> snapshot){
    // 接收到 controller 傳送給 stream 的資料
    return Text('${snapshot.data}');
  }
),
)
複製程式碼

我們可以通過_streamController 傳送資料, 然後會自動呼叫 StreamBuilder builder 回撥, 從而重新整理 Widget

_streamController.sink.add(++_counter);
複製程式碼

當然也可以不通過 StreamController 來提供 stream, 也可以建立一個函式返回 stream, 具體如何建立可以檢視我之前的文章 《(六)Flutter 學習之 Dart 非同步操作詳解》

聯絡我

下面是我的公眾號,乾貨文章不錯過,有需要的可以關注下,有任何問題可以聯絡我, 也可以在文章下面給我留言:

公眾號:  chiclaim

相關文章