Flutter 入門與實戰(三十):Dio之戛然而止

島上碼農發表於2021-07-17

甜蜜的約會

程式設計師小明今天很開心,因為今天是他和女朋友的戀愛一週年紀念日。眾所周知,程式設計師要找女朋友是很難的,小明目前是他們辦公室唯一脫單的程式設計師,成為了眾多程式設計師豔羨的物件。小明今天的工作效率也很高,鍵盤敲得都要飛起來了,整個辦公室都響著他快樂的鍵盤敲擊聲。到了下班時間,小明早已提交好程式碼,飛奔下樓奔赴約會了——當然,他沒有忘記買花。

女朋友見了小明也是非常開心,這一天的安排倆人也早就計劃好了。一對小年輕回憶起這一年的經歷,甜言蜜語說個不停——倆人引起了不少揹著雙肩包、穿著格子衫的人羨慕的眼光。

dating.jpg

電話響了

到了晚上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 了(小明心中一萬頭草泥馬飄過)!

螢幕錄製2021-07-17 下午3.59.57.gif

實際應用

假設我們退出頁面前要取消未完成的請求,就可以使用 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 方法。

image.png

我們下一篇來研究一下 Dio 這塊的原始碼,看看 CancelToken 的實現機制。

後記

忠告各位程式設計師,重要的話說三遍:約會請記得關機!約會請記得關機!約會請記得關機!

相關文章