Flutter 入門與實戰(三十四): Dio之檔案下載

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

前言

檔案下載在很多型別的應用中會涉及,例如音樂、文件、包括圖片(只是圖片可以使用一些元件完成無感知的下載)。本篇介紹使用 Dio 的下載方法完成檔案的下載,涉及到的內容如下:

  • Dio 外掛的 download 方法介紹;
  • 使用 download 的回撥方法監測下載進度;
  • 使用 CancelToken 取消正在下載的任務;
  • 刪除已下載的檔案;
  • path_provider 外掛管理App檔案目錄;
  • 下載檔案除錯過程中發現的一些問題;

Dio 的下載方法 download

Dio 的下載方法定義如下:

Future<Response> download(
  String urlPath,
  savePath, {
  ProgressCallback? onReceiveProgress,
  Map<String, dynamic>? queryParameters,
  CancelToken? cancelToken,
  bool deleteOnError = true,
  String lengthHeader = Headers.contentLengthHeader,
  data,
  Options? options,
});
複製程式碼
  • urlPath:網路資源的 url
  • savePathdynamic 型別,可以是下載後儲存檔案路徑的字串,也可以是一個返回字串的回撥方法(Dio 會把 headers 引數攜帶過去,方便針對下載返回內容構建檔案路徑);
  • onReceiveProgress:檔案接收進度,是一個void Function(int count, int total)回撥函式,呼叫者可以通過該回撥方法監測下載進度。
  • deleteOnError:發生錯誤時候是否刪除已下載的檔案,預設是 true。
  • lengthHeader:原始檔的實際大小(未壓縮前)。預設是 headercontent-length。如果檔案壓縮了,而沒有指定該值的話,那進度回撥裡的total會是-1;如果使用自定義的 header 指定了檔案的大小,那麼total會是自定義的 header 對應的檔案大小。
  • 其他引數和普通的請求差不多,這裡不再贅述。

我們還是繼續學習老王(老王的故事看這篇:Flutter 入門與實戰(二十八):Dio 封裝之金屋藏嬌),將 Dio 隱藏起來,在 http_util.dart 中封裝一個我們自己的下載方法。

static Future download(
  String url,
  String savePath, {
  Map<String, dynamic> queryParams,
  CancelToken cancelToken,
  dynamic data,
  Options options,
  void Function(int, int) onReceiveProgress,
}) async {
  try {
    return await _dioInstance.download(
      url,
      savePath,
      queryParameters: queryParams,
      cancelToken: cancelToken,
      onReceiveProgress: onReceiveProgress,
    );
  } on DioError catch (e) {
    if (CancelToken.isCancel(e)) {
      EasyLoading.showInfo('下載已取消!');
    } else {
      if (e.response != null) {
        _handleErrorResponse(e.response);
      } else {
        EasyLoading.showError(e.message);
      }
    }
  } on Exception catch (e) {
    EasyLoading.showError(e.toString());
  }
}
複製程式碼

監測下載進度

我們新建一個檔案下載頁面 file_download.dart完成檔案下載的示例。這裡定義了幾個屬性來對檔案下載過程進行反饋:

// 檔案下載地址,這裡是谷歌瀏覽器的下載地址(Mac 版本)
String _downloadPath =
      'https://dl.google.com/chrome/mac/stable/GGRO/googlechrome.dmg';
// 下載進度比例,用於檢測下載是否完成
double _downloadRatio = 0.0;
// 下載進度百分比
String _downloadIndicator = '0.00%';
// 下載檔案的儲存路徑
String _destPath;
// 取消下載的 token
CancelToken _token;
// 指示當前是否處於下載中,以便做業務判斷
bool _downloading = false;
複製程式碼

然後我們定義一個下載方法,在下載過程中如果 total 不為-1就更新下載進度,否則提示錯誤(實際除錯發現,如果涉及到需要驗證的,下載後後端實際會返回網頁,這樣也能下載網頁內容下來,但是不是想要的檔案)。

void _downloadFile() {
  _token = CancelToken();
  _downloading = true;
  HttpUtil.download(_downloadPath, _destPath, cancelToken: _token,
      onReceiveProgress: (int received, int total) {
    if (total != -1) {
      if (!_token.isCancelled) {
        setState(() {
          _downloadRatio = (received / total);
          if (_downloadRatio == 1) {
            _downloading = false;
          }
          _downloadIndicator =
              (_downloadRatio * 100).toStringAsFixed(2) + '%';
        });
      }
    } else {
      _downloading = false;
      EasyLoading.showError('無法獲取檔案大小,下載失敗!');
    }
  });
}
複製程式碼

這裡因為涉及到可能取消,因此只有在沒有取消的情況下才更新下載狀態,要不可能會出現取消的時候還處在下載接收位元組的過程中,雖然取消了但是看到下載進度還在走的情況。

取消下載

取消下載其實很簡單,當我們點選取消按鈕的時候,呼叫 CancelTokencancel方法即可。這裡我們做了一個判斷,下載比例低於1才可以取消,因為下載完成再取消會拋異常。同時取消後重置下載比例和顯示的下載百分比。

void _cancelDownload() {
  if (_downloadRatio < 1.0) {
    _token.cancel();
    _downloading = false;
    setState(() {
      _downloadRatio = 0;
      _downloadIndicator = '0.00%';
    });
  }
}
複製程式碼

刪除已經下載的檔案

對於 App,沒有別的入口管理檔案,因此實際過程中我們需要提供下載入口共使用者清理已下載的檔案。實際已下載的檔案,我們需要有下載檔案管理功能供使用者管理檔案,這個時候會需要本地儲存支撐,我們在後續的章節會介紹本地儲存。 ​

刪除檔案前需要判斷檔案是否存在,如果檔案不存在刪除可能丟擲異常。檔案的管理使用的是 dart:io 中的方法。

void _deleteFile() {
  try {
    File downloadedFile = File(_destPath);
    if (downloadedFile.existsSync()) {
      downloadedFile.delete();
    } else {
      EasyLoading.showError('檔案不存在');
    }
  } catch (e) {
    EasyLoading.showError(e.toString());
  }
}
複製程式碼

path_provider檔案目錄管理

在 App 中沒法直接知道應用的檔案儲存目錄,因此需要借用 path_provider 外掛來獲取 App 的檔案儲存目錄,path_provider 提供瞭如下方法:

  • getTemporaryDirectory:應用臨時目錄(可能被清除)
  • getApplicationDocumentsDirectory:應用文件目錄(不會被系統清除,主要使用者資料儲存目錄),對於安卓推薦使用外部儲存getExternalStorageDirectory。
  • getApplicationSupportDirectory:應用支援目錄,一般放置與使用者無關的資料。
  • getLibraryDirectory:指向應用可以持久儲存資料的目錄,不支援安卓平臺。
  • getExternalStorageDirectory:獲取外部儲存目錄,不支援 iOS 平臺。
  • getExternalCacheDirectories:獲取外部快取目錄,,不支援 iOS 平臺。
  • getExternalStorageDirectories:獲取外部可以的目錄列表,不支援 iOS 平臺。
  • getDownloadsDirectory:獲取下載目錄,用於 Web 端,不支援安卓和 iOS平臺。

通過 path_provider拿到Directory物件後,就可以通過 Directorypath 屬性獲取到完整的目錄路徑。本例我們是在 initialState 裡獲取檔案儲存路徑的,使用的是臨時目錄。

void initState() {
  getTemporaryDirectory()
      .then((tempDir) => {_destPath = tempDir.path + 'googlechrome.dmg'});

  super.initState();
}
複製程式碼

除錯過程中遇到的一些錯誤

  • OS Error: Read-only file system:安卓系統需要獲取READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE許可權。同時需要使用 path_provider獲取應用的檔案目錄再往對應的目錄讀寫檔案和訪問檔案目錄。
  • onReceivedProgress 中如果total=-1則表示該檔案被壓縮或者需要會話資訊才可以下載(如後端開啟了驗證)。
  • 刪除檔案的時候需要檢查檔案是否在下載過程中,如果在下載過程中刪除會引起檔案讀寫衝突,丟擲異常。
  • CancelToken一個例項只能取消一次請求,因此每次發起請求的時候需要重新構建CancelToken物件,否則取消一次後無法再次取消。

執行結果及程式碼

執行結果如下圖所示:

螢幕錄製2021-07-24 下午9.59.04.gif

程式碼已提交至 gitee:Dio網路請求相關程式碼

總結

從示例可以看到,Dio 的下載方法是簡單易用的,而且提供了友好的下載反饋。同時,藉助 CancelToken 也能取消或者暫停下載(暫停時設定deleteOnError=false,表示不刪除檔案,然後恢復到時候從已下載的偏移量開始,也可以按這種方式做斷點續傳,具體方式可以搜尋或者自己完成)。Dio 網路請求系列的詳細介紹到這一篇結束,後續實際業務用到的時候再穿插介紹,接下來一篇將對 Dio 系列文章做一個整體總結。

相關文章