前言
在實際開發中, 一般在展示列表內容之前需要先展示一個 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,
...
)
複製程式碼
執行效果如下圖所示:
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 非同步操作詳解》
聯絡我
下面是我的公眾號,乾貨文章不錯過,有需要的可以關注下,有任何問題可以聯絡我, 也可以在文章下面給我留言: