甜蜜的約會
程式設計師小明今天很開心,因為今天是他和女朋友的戀愛一週年紀念日。眾所周知,程式設計師要找女朋友是很難的,小明目前是他們辦公室唯一脫單的程式設計師,成為了眾多程式設計師豔羨的物件。小明今天的工作效率也很高,鍵盤敲得都要飛起來了,整個辦公室都響著他快樂的鍵盤敲擊聲。到了下班時間,小明早已提交好程式碼,飛奔下樓奔赴約會了——當然,他沒有忘記買花。
女朋友見了小明也是非常開心,這一天的安排倆人也早就計劃好了。一對小年輕回憶起這一年的經歷,甜言蜜語說個不停——倆人引起了不少揹著雙肩包、穿著格子衫的人羨慕的眼光。
電話響了
到了晚上9點多,倆人的浪漫晚餐結束的時候,小明的女朋友說:“我們們回去吧!”,小明心領神會,正準備牽女朋友手往外走的時候,電話響了!電話響了!電話響了!小明心裡預感不妙,收回剛要伸出的手,從口袋裡掏出了手機。看到手機上顯示的名字,他要崩潰了!那是他們領導打來的電話。 “小明,趕緊回公司,你今天提交的程式碼出Bug 了!” “呃,很緊急嗎?” “緊急啊!不緊急用得著打電話給你嗎?” “那……那我馬上回去。” 小明無奈地看了一眼女朋友。 “領導讓我回去改 Bug!” “不能明天再去嗎?我們正在約會唉!”女朋友一臉不高興。 “不行哦!我們程式設計師發現 Bug 要馬上改!” 小明說完,背起他的雙肩包趕緊往公司趕去,留下女朋友一個人呆呆地站在那裡。小明沒有聽見女朋友的一句話:“我覺得我們不合適……”
言歸正傳
上述的故事在我們的日常生活很常見,改 Bug 嘛,那不是我們程式設計師賴以生存的根基麼?實際上,一件事情被打斷經常發生,在網路請求裡也一樣。比如說,剛進入一個頁面,網路請求還在進行,然後使用者又退出這個頁面了。那這時候的請求其實沒什麼意義,如果是簡單的請求還好,但如果是下載檔案、進行一系列請求的時候,如果能夠取消請求就好了。
Dio 提供了取消令牌(CancelToken)機制用於取消尚未完成的請求。每個請求都可以攜帶一個 CancelToken 物件,當呼叫 CancelToken 的 cancel 方法時,就會通知該請求停止當前的請求,從而達到中斷請求的目的。這就好比是小明正在約會的時候,被領導的電話叫去改 Bug 一樣,他的約會相當於泡湯了!
CancelToken 的使用
我們先看一個簡單的示例,首先在我們的 HttpUtil 類的各類方法都加上了可選的引數 cancelToken,並且在檢測到取消後顯示對應的提示。
static Future sendRequest(HttpMethod method, String url,
{Map<String, dynamic> queryParams,
dynamic data,
CancelToken cancelToken}) async {
try {
//...省略請求程式碼
} on DioError catch (e) {
// 檢測錯誤是不是因為取消請求引起的,如果是列印取消提醒
if (CancelToken.isCancel(e)) {
EasyLoading.showInfo('領導喊你回去改 Bug 啦!');
} else {
EasyLoading.showError(e.message);
}
} on Exception catch (e) {
EasyLoading.showError(e.toString());
}
return null;
}
複製程式碼
然後我們模擬一個取消請求的情況。我們請求的是掘金的個人主頁的文章列表,首先建立了一個 CancelToken 物件,然後傳給對應的請求。發出請求後,我們馬上呼叫了 cancel方法取消請求。這裡不能使用 async 和 await。因為 await 會等待請求完成,因此這裡使用的是 Future 的 then 方法(類似 Promise)。接收到響應後,我們根據 CancelToken 物件的 isCancelled 屬性來判斷請求是否被取消,並且更新狀態變數_hasBug。
void _bugHappened() {
CancelToken token = CancelToken();
JuejinService.listArticles('70787819648695', cancelToken: token)
.then((value) => {
setState(() {
_hasBug = token.isCancelled;
})
})
.onError((error, stackTrace) => {print(error.toString())});
token.cancel();
}
複製程式碼
_hasBug 用於控制介面顯示,表示是否有 Bug,如果沒有 Bug 那小明可以繼續約會,如果有 Bug 那小明得趕回公司改 Bug。我們通過一個按鈕來觸發 bug 事件。
class _AppointmentPageState extends State<AppointmentPage> {
bool _hasBug = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_hasBug ? '該死的Bug!' : '約會中...',
style: Theme.of(context).textTheme.headline4),
),
body: Container(
child: Center(
child: Image.asset(_hasBug ? 'images/bug.png' : 'images/dating.png'),
),
),
floatingActionButton: IconButton(
icon: Icon(Icons.call),
onPressed: () {
_bugHappened();
},
),
);
}
//...
}
複製程式碼
執行結果
執行結果如下圖所示,可以看到點選按鈕後出現了請求被取消事件——小明得回去改 Bug 了(小明心中一萬頭草泥馬飄過)!
實際應用
假設我們退出頁面前要取消未完成的請求,就可以使用 CancelToken 來取消了。我們可以在 State生命週期函式的 deactivate 方法(該方法在 dispose 前會被呼叫)呼叫 CancelToken 的取消方法。為了演示效果,我們來一個請求比較慢的網站——Github。 由於 CancelToken 物件在不同的方法使用,因此需要定義為成員屬性,完整程式碼如下。
class _AppointmentPageState extends State<AppointmentPage> {
bool _hasBug = false;
CancelToken _token;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_hasBug ? '該死的Bug!' : '約會中...',
style: Theme.of(context).textTheme.headline4),
),
body: Container(
child: Center(
child: Image.asset(_hasBug ? 'images/bug.png' : 'images/dating.png'),
),
),
floatingActionButton: IconButton(
icon: Icon(Icons.call),
onPressed: () {
_bugHappened();
},
),
);
}
void _bugHappened() {
_token = CancelToken();
HttpUtil.get('https://www.github.com', cancelToken: _token)
.then((value) => {
if (mounted)
{
setState(() {
_hasBug = _token.isCancelled;
})
}
})
.onError((error, stackTrace) => {});
}
@override
void deactivate() {
if (_token != null) {
_token.cancel('dispose');
}
super.deactivate();
}
}
複製程式碼
業務流程如下:
- 進入介面後,點選底部的電話圖示按鈕開始請求
- 點選返回介面時檢查_token 是否為空,不為空則呼叫 cancel 方法取消請求。cancel 方法可以接收一個可選的引數,用於表名取消的原因。
如果網路狀況不太好而你的手速有足夠快的話(打王者的技巧派上用場了),就可以看到返回後會顯示一個提醒,說明我們的請求被取消了。
這裡需要注意,由於我們在網路請求的 then 回撥呼叫了 setState 方法,這個方法在 dispose後是不能呼叫的,否則可能導致記憶體洩露。因此在呼叫前我們判斷了一下 mounted 是否為 true,如果是則表示沒有被 dispose,可以安全地呼叫 setState 方法。
我們下一篇來研究一下 Dio 這塊的原始碼,看看 CancelToken 的實現機制。
後記
忠告各位程式設計師,重要的話說三遍:約會請記得關機!約會請記得關機!約會請記得關機!