背景
好久沒有更文了,為避免讓廣大朋友產生我消失了的錯覺,所以就又水一文,能幫助一個是一個。 本來想自己寫寫文章,後來發現翻譯也不錯。這裡是英文原文。
簡述
final myFuture = http.get("https://example.com");
複製程式碼
正如上面的程式碼,很多Dart非同步API都是返回的Future。Future
也是Dart
非同步程式設計中最基礎的一個概念。總地來說,Dart
中的Future
和其他程式語言中的future
或者promise
大同小異。
本文將圍繞Future
背後的概念以及如何使用Future
展開。也會講Flutter
中的FutureBuilder
控制元件,這個控制元件會基於Future
狀態來幫助你非同步地更新Flutter UI
。
得益於Dart
像async-await這樣的特性,我們可能永遠不會直接使用Future
這個API。但是我們也很難避免在Dart
程式碼中遇到Future
,畢竟我們可能要建立Future
或者閱讀一些和Future
有關的程式碼。
如何理解Future
我們可以簡單地將資料比喻成禮物。現在有個朋友要送給我們一個禮盒,當禮盒裝好的那一刻,這個過程就開始了。一段時間後我們需要開啟這個神祕盒子,裡面的禮物可能完好無損也可能是損壞了,也就是說當一個Future
完成後,對應的結果可能是我們期望的資料,也可能是一個錯誤。
我們可以這個過程歸納成三種狀態:
- 未完成:禮盒封裝好了。
- 完成了並且得到了對應的值:禮盒開啟,並且我們的禮物(data)已經準備好了。
- 完成了但發生了錯誤:禮盒開啟,但禮物卻損壞了(error)。
絕大部分情況下,我們無非都是圍繞著這三種狀態進行一些處理。當我們接收一個Future
時,我們會一直等到我們開啟禮盒,然後我們才會決定如何處理,比如說可以正常接收到值時,我們應該如何處理,又或者說,當發生錯誤的時候我們又要怎麼做。我們可以經常看到1-2-3這樣的過程:
說到Future
我們不得不提到Event-Loop
(如下圖所示,也可以在上面的系列視訊中學習)。關於Future
,我們需要知道,Future
只是一個幫助我們可以更簡單使用Event-Loop
的API。
我們寫的Dart
程式碼是由單一執行緒執行的。當我們的應用在執行時,這個執行緒一直在不停地執行啊執行,然後不停得從事件佇列(Event Queue)中拾取事件並對事件進行處理。
為了更好解釋Event-Loop
和Future
,我們看個簡單的例子。
假如說,我們要實現一個下載功能,當使用者點選了按鈕,程式會自動下載一下圖片。,我們用RaisedButton
簡單實現一下:
RaisedButton(
onPressed: () {
final myFuture = http.get('https://my.image.url');
myFuture.then((resp) {
setImage(resp);
});
},
child: Text('Click me!'),
)
複製程式碼
我們一起理理這個過程。
首先,觸發點選按鈕事件。Event Loop
捕獲了點選事件,然後呼叫了點選監聽器(就是在RaisedButton
建構函式中傳入的onPressed
)。我們的onPressed
使用了http
庫進行了一次HTTP
請求(http.get()
),並且這個請求返回了一個Future
(myFuture
)。
現在我們已經得到了我們的禮盒--myFuture
。現在禮盒已經裝好了。為了監聽開啟禮盒的回撥,我們要使用then()
。
一旦我們裝好了禮盒,我們就需要等待。也話在這個期間有其他的事件進入Event-Loop
,使用者做了一些其他的事情,你的禮盒還放在那裡的同時,Event-Loop
依然保持執行。
最終,圖片資料被下載下來,然後http
庫會告訴我們“好極了,我已經拿了Future
”。然後它把資料裝進禮盒中並把禮盒開啟,這時就觸發了我們的回撥。
現在,then()
中的程式碼片斷就被執行了,並向使用者顯示圖片。
整個過程中,不管有什麼其他的任務正在進行或者有其他任務進入,我們的程式碼從來也沒有直接接觸Event-Loop
。這個過程不需要關心有什麼其他任務在進行,或者有什麼其他事件進入。我們所要作的就是從http
庫得到Future
,然後告訴程式在Futuer
完成後需要做什麼。
在現實的程式碼中,我們還要關注錯誤。我們稍後涉及到這點。
現在我們更近一步地瞭解一下Future
API,有些正是我們剛剛看到的。
第一個問題,我們怎麼得到一個Future
例項?大部分情況下,我們不會直接建立Future
。因為大部分常見的非同步程式設計任務已經有對應的庫了,這些庫可以為我們生成Future
。
比如說,網路請求返回了一個Future
:
final myFuture = http.get('http://example.com');
複製程式碼
得到一個shared preferences
也返回一個Future
:
final myFuture = SharedPreferences.getInstance();
複製程式碼
當然了,我們也可以通過Future
的構造方法建立Future
。
Future構造方法
最簡單的Future
構造方法是Future()
, 這個構造方法的引數是一個函式,並且返回一個和該函式返回值型別一樣的Future
。過一會這個函式會非同步地執行,並且這個Future
完成時會返回該函式的值。看一下Future()
的例子:
void main() {
final myFuture = Future(() {
return 12;
});
}
複製程式碼
讓我們加入一些列印語句,這樣會讓非同步部分更加明顯:
void main() {
final myFuture = Future(() {
print('Creating the future.'); // Prints second.
return 12;
});
print('Done with main().'); // Prints first.
}
複製程式碼
如果我們在DartPad上執行這段程式碼,整個main
函式會在傳給Future()
構造方法的函式結束前結束。這是因為Future()
的構造方法恰好先返回了一個未完成的Future
。這意味著,“這是個盒子。你現在需要拿著他,然後過一會我會執行你的函式並且把一些資料裝進去”,我們再看一下之前程式碼的輸出結果:
Done with main().
Creating the future.
複製程式碼
另一個構造方法是Future.value()
,它是用來處理你已經知道Future
返回值的。這個構造方法在我們構建使用了快取的服務時很有用。有的時候我們已經持有我們需要地值了,所以我們可以直接返回:
final myFuture = Future.value(12);
複製程式碼
Future.value()
還一個相對立構造方法,這個構造方法在完成時會返回一個錯誤。它是Future.error()
,而它的工作原理也基本相同,但是這個構造方法會承載一個錯誤物件和一個可選的stacktrace
:
final myFuture = Future.error(ArgumentError.notNull('input'));
複製程式碼
最方便的構造方法可能是Future.delay()
了。它和Future()
一樣,只不過它先會等待一段指定的時間後,再執行傳入的函式然後再完成Future
。
Future.delay()
的一種使用場景就是當我們在測試時需要mock
網路服務。如果我們確保載入指示器可以正確顯示,那麼這個Future.delay()
就有用武之地了:
final myFuture = Future.delayed(
const Duration(seconds: 5),
() => 12,
);
複製程式碼
使用Future
我們現在已經對Future
有個基本地瞭解,現在我們要學習一下怎麼使用。正好我們之前所說,使用Future
基本上就是圍繞著三種狀態:未完成
、完成了並且得到了對應的值
和完成了但發生了錯誤
。
下面的程式碼使用Future.delay()
建立了一個Future
,功能是在3s後Future
完成並返回100。
void main() {
Future.delayed(
const Duration(seconds: 3),
() => 100,
);
print('Waiting for a value...');
}
複製程式碼
當這段程式碼執行時,main()
從上到下執行,建立一個Future
並列印Waiting for a value...
,這個時候Future
還沒完成。再過三秒鐘它也不會完成。
為了使用Future
返回的值,我們可以使用then()
。我們可以通過then()
註冊一個回撥,通過這個回撥我們可以獲取Future
完成時的資料。我們給then()
傳入一個函式,這個函式只有一個引數,引數的型別和Future
的返回值型別一樣。一旦Future
完成了返回並返回資料,這個函式就會被呼叫並將對應的資料傳遞過去:
void main() {
Future.delayed(
const Duration(seconds: 3),
() => 100,
).then((value) {
print('The value is $value.'); // Prints later, after 3 seconds.
});
print('Waiting for a value...'); // Prints first.
}
複製程式碼
讓我們看一下輸出日誌:
Waiting for a value... (3 seconds pass until callback executes)
The value is 100.
複製程式碼
除了執行我們的程式碼,then()
本身也會返回自己的Future
,與我們提供的函式的返回值一樣。所以如果我們需要進行一連串的非同步呼叫,我們可以選擇鏈式呼叫他們,儘管他們的返回型別不同:
_fetchNameForId(12)
.then((name) => _fetchCountForName(name))
.then((count) => print('The count is $count.'));
複製程式碼
回到我們第一個例子中,如果初始化的Future
完成時沒有對應的資料會發生什麼--也就是說如果發生了錯誤什麼如何?而then()
方法是需要一個值的。這時我們需要註冊另一個回撥來處理髮生錯誤的情況。
答案是使用[catchError()](https://api.dart.dev/stable/2.8.2/dart-async/Future/catchError.html)
。它和then()
一樣,只不過catchError()
傳遞的不是資料,當Future
執行過程中如果發生了錯誤,這個方法會被呼叫。就像then()
一樣,catchError()
本身也返回一個自己的Future
,所以我們可以構建一個then()
和catchError()
的呼叫鏈,這兩個方法可以相互待。
筆記:如果你在程式碼中使用了
async-await
,我們就不必使用then()
和catchError()
了。因為我們可以使用await
直接獲取對應的值,可以用try-catch-finally
來處理錯誤。更詳細的資料,看一下Dart官方文件中關於非同步支援的章節吧。
下面的例子展示瞭如何用catchError()
處理Futuer
中的錯誤:
void main() {
Future.delayed(
Duration(seconds: 3),
() => throw 'Error!', // Complete with an error.
).then((value) {
print(value);
}).catchError((err) {
print('Caught $err'); // Handle the error.
});
print('Waiting for a value...');
}
複製程式碼
我們甚至可以給catchError()
一個測試函式,這個函式可以在回撥呼叫前對錯誤進行測試。通過這種方式,我們可以擁有多個catchError()
函式,每個函式檢查一種不同型別的錯誤。下面的例子展示瞭如何使用一個檢測函式對錯誤進行測試,catchError()
中的引數test
是可選的。
void main() {
Future.delayed(
Duration(seconds: 3),
() => throw 'Error!',
).then((value) {
print(value);
}).catchError((err) {
print('Caught $err');
}, test: (err) { // Optional test parameter.
return err is String;
});
print('Waiting for a value...');
}
複製程式碼
現在文章你已經看到這裡了,希望你已經理解了Future
的三種狀態,已經理解了三種狀態是怎麼在程式碼中體現的。在上面的例子中分三塊:
- 第一塊建立了一個未完成的
Future
。 - 當
Future
執行完成並返回了對應資料,呼叫了then()
裡的回撥。 - 當
Future
執行完成了但發生錯誤,呼叫了catchError()
裡的回撥。
還有另一個方法你也可能想使用:whenComplete()
。正如方法名字一樣,這個方法會在Future
完成時被呼叫,不管Future
是返回了資料還是丟擲了錯誤。
這有點像try-catch-finally
中的finally
。無論是程式碼是正確執行了,還是出現了錯誤,它都會被執行。
在Flutter中使用Future
我們剛剛一直在說如何建立Future
,還有如何使用Future
中的資料。現在我們要說說如何在Flutter
中實踐。
假如說我們一個網路服務,這個網路服務會返回JSON
資料,我們想要展示這些資料。我們可以當然使用StatefulWidget
,然後通過建立Future
,根據Future
的執行情況,然後呼叫setState()
,所有的這些都要我們手動控制。
當然了我們也可以使用FutureBuilder
。這是一個Flutter SDK
中的一個控制元件。我們傳一個Future
和一個builder
函式,當Future
完成時,它會自動重構它的子控制元件。
FutureBuilder
會呼叫builder
函式,這個函式有兩個引數,一個是context
,另一個是snapshot
,它是當前Future
的狀態。
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Use a FutureBuilder.
return FutureBuilder<String>(
future: _fetchNetworkData(),
builder: (context, snapshot) {},
);
}
}
複製程式碼
我們可以通過檢查snapshot
來看看Future
是否發生了錯誤:
return FutureBuilder<String>(
future: _fetchNetworkData(5),
builder: (context, snapshot) {
if (snapshot.hasError) {
// Future completed with an error.
return Text(
'There was an error',
);
}
throw UnimplementedError("Case not handled yet");
},
);
複製程式碼
我們也可以通過檢查hasData
檢視是否返回了資料:
return FutureBuilder<String>(
future: _fetchNetworkData(5),
builder: (context, snapshot) {
if (snapshot.hasError) {
// Future completed with an error.
return Text(
'There was an error',
);
} else if (snapshot.hasData) {
// Future completed with a value.
return Text(
json.decode(snapshot.data)['field'],
);
}
throw UnimplementedError("Case not handled yet");
},
);
複製程式碼
如果hasError
和hasData
都不是true
,那麼我們就知道Future
還在執行中,我們還需要繼續等待,這時我們也可以輸出一些其他的資訊:
return FutureBuilder<String>(
future: _fetchNetworkData(5),
builder: (context, snapshot) {
if (snapshot.hasError) {
// Future completed with an error.
return Text(
'There was an error',
);
} else if (snapshot.hasData) {
// Future completed with a value.
return Text(
json.decode(snapshot.data)['field'],
);
} else {
// Uncompleted.
return Text(
'No value yet!',
);
}
},
);
複製程式碼
即使在Flutter
程式碼中我們依然可以看到三種狀態是如何體現的。
總結
本文闡述了Future
是如何呈現的以及怎麼樣使用Future
和FutureBuilder
相關API建立Future
,並且使用Future
中的資料。
如果你想學習更多關於如何使用Future
,可以通過執行示例程式碼和互動練習來測試你對Future
的理解--去codelab
上練一下futures,aysnc,await
吧。