Flutter實現檔案下載

僱個城管打天下發表於2019-07-13

前言

之前有做一個工具集的微信小程式「開掛Lite」,但是由於小程式自身限制,沒有辦法實現下載檔案的功能,只能把下載連結解析出來。而且受限於微信平臺,小程式的稽核是一件很麻煩的事情,因此有了將其APP化的想法。

自從去年Flutter橫空出世後,我便一直關注它的發展,時隔一年後重新拾起,發現它的生態已經初具規模,於是決定採用Flutter重做一個「開掛Lite」。後期我也會不定時更新一些和Flutter有關的文章,希望大家可以多多支援。本文記錄的便是我利用Flutter實現檔案下載功能的過程。

完整原始碼可在公眾號:「01二進位制」後臺回覆:「Flutter 檔案下載」獲取

開始

我們先看一下實現的效果:

iOS

Flutter實現檔案下載

Android

Flutter實現檔案下載

本demo的實現效果非常簡單,就是點選一個按鈕,然後下載檔案,完成後提示使用者是否開啟檔案。

準備工作

在本 demo 中使用的 IDE 為 Android Studio,同時使用到了以下幾個庫:

flutter_downloader: ^1.1.7
path_provider: 1.1.2
permission_handler: ^3.1.0
progress_dialog: ^1.1.0+1
toast: ^0.1.4
複製程式碼

我們先新建一個空專案,然後將上述依賴新增到專案的pubspec.yaml檔案,新增位置如下:

Flutter實現檔案下載

接下來我們可以在 Terminal 中輸入flutter packages get或者點選 IDE 左上角的Packages get字樣安裝依賴。

Flutter實現檔案下載

然後將初始專案中的多餘程式碼刪除,並在中間新增一個按鈕。

body: Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      RaisedButton(
        child: Text("點我下載檔案"),
        onPressed: () {
          // 執行下載操作
          _doDownloadOperation();
        },
      ),
    ],
  ),
),
複製程式碼

其中_doDownloadOperation()便是我們執行下載操作的方法,至此,前期準備工作結束。

邏輯分析

雖然整個下載演示的過程非常簡單,但還是有必要來分析整個下載的流程,如下圖所示:

Flutter實現檔案下載

所以我們接下來要做的事情便是:

  1. 獲取許可權:網路許可權、儲存許可權
  2. 獲取下載路徑
  3. 設定下載回撥(用於監聽下載過程)

操作

獲取許可權

這裡使用到一個許可權獲取外掛:permission_handler,這個外掛提供了跨平臺(Android和iOS)的許可權檢查以及獲取API,地址在:pub.flutter-io.cn/packages/pe…

開啟專案根目錄下的android/app/src/main/AndroidManifest.xml檔案,位置如下圖所示:

Flutter實現檔案下載

然後新增我們需要使用的許可權的申明,如下圖所示:

Flutter實現檔案下載

接下來我們就可以寫程式碼來獲取所需的許可權了。建立一個_checkPermission()函式用於判斷許可權是否給予。當然由於平臺差異,我們需要判斷其為Android平臺,申請程式碼如下:

// 申請許可權
Future<bool> _checkPermission() async {
  // 先對所在平臺進行判斷
  if (Theme.of(context).platform == TargetPlatform.android) {
    PermissionStatus permission = await PermissionHandler()
        .checkPermissionStatus(PermissionGroup.storage);
    if (permission != PermissionStatus.granted) {
      Map<PermissionGroup, PermissionStatus> permissions =
          await PermissionHandler()
              .requestPermissions([PermissionGroup.storage]);
      if (permissions[PermissionGroup.storage] == PermissionStatus.granted) {
        return true;
      }
    } else {
      return true;
    }
  } else {
    return true;
  }
  return false;
}
複製程式碼

獲取下載路徑

這裡是使用的外掛是path_provider,它是一個配合Dart的IO庫以便在Flutter中實現檔案讀寫的外掛,Flutter中文網對該外掛有著詳細的介紹(flutterchina.club/reading-wri…),這裡我們需要明白一個問題,就是iOS沒有外接儲存這一概念,因此需要對平臺進行判斷,程式碼如下:

// 獲取儲存路徑
Future<String> _findLocalPath() async {
  // 因為Apple沒有外接儲存,所以第一步我們需要先對所在平臺進行判斷
  // 如果是android,使用getExternalStorageDirectory
  // 如果是iOS,使用getApplicationSupportDirectory
  final directory = Theme.of(context).platform == TargetPlatform.android
      ? await getExternalStorageDirectory()
      : await getApplicationSupportDirectory();
  return directory.path;
}
複製程式碼

通過上述程式碼我們便可以獲取儲存路徑,但是如果我們不想把檔案下載到儲存路徑呢?比如我就喜歡單獨設定一個/Download路徑專門用於儲存下載檔案,其實也很簡單:

// 獲取儲存路徑
var _localPath = (await _findLocalPath()) + '/Download';

final savedDir = Directory(_localPath);
// 判斷下載路徑是否存在
bool hasExisted = await savedDir.exists();
// 不存在就新建路徑
if (!hasExisted) {
  savedDir.create();
}
複製程式碼

下載檔案

下載檔案這裡我找了一些資料,發現貌似只有一個flutter_downloader外掛,也不知道是什麼情況。該外掛的配置過程也是挺複雜的,好在文件(pub.flutter-io.cn/packages/fl…)寫的還算明白。這個外掛可以實現後臺下載,分別基於 Android 中的 WorkManager 和 iOS 中的 NSURLSessionDownloadTask 實現的。

接下來分別說下在iOS端和Android端的設定。

外掛配置

iOS端配置

  • 啟用 background mode

想要執行這一步,我們在Xcode中開啟該專案的 iOS module,如下圖所示:

Flutter實現檔案下載

然後雙擊左側Runner選項,選擇 Capabilities 選項,按圖中所示啟用background mode

Flutter實現檔案下載

Flutter實現檔案下載

  • 新增 sqlite 依賴庫

Flutter實現檔案下載

Flutter實現檔案下載

文件中還提供了一些可選配置

  • 設定 HTTP 請求支援

為了安全起見,蘋果官方已經預設不讓開發者使用不安全的http通訊協議了,而是建議開發者使用安全的https協議。若我們還是需要使用 http 協議需要做一些配置,文件中給了兩種方式配置,一種是允許單個HTTP請求的域名,另一種是允許所有HTTP請求的域名,這裡出於演示目的,選擇第二種。

只需要在Info.plist檔案中新增如下程式碼即可:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key><true/>
</dict>
複製程式碼
  • 設定最大同時下載數

預設支援同時下載最多3個檔案,如果你需要更改同樣需要更改Info.plist

<key>FDMaximumConcurrentTasks</key>
<integer>5</integer>
複製程式碼
  • 設定下載完成通知

同樣的,修改Info.plist

<key>FDAllFilesDownloadedMessage</key>
<string>All files have been downloaded</string>
複製程式碼

Android端配置

說完了iOS端的配置,我們再來說下Android端的配置。在 AndroidManifest.xml 檔案中新增如下程式碼:

<provider
    android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider"
    android:authorities="${applicationId}.flutter_downloader.provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths"/>
</provider>
複製程式碼

位置如下:

WX20190713-174805@2x

還有其他類似於iOS端的可選配置,功能大同小異,這裡就不說了,詳見官網。

編寫下載程式碼

配置結束後,其實下載的程式碼很簡單:

// 根據 downloadUrl 和 savePath 下載檔案
_downloadFile(downloadUrl, savePath) async {
  await FlutterDownloader.enqueue(
    url: downloadUrl,
    savedDir: savePath,
    showNotification: true,
    // show download progress in status bar (for Android)
    openFileFromNotification:
        true, // click on notification to open downloaded file (for Android)
  );
}
複製程式碼

當然我們需要提前引入flutter_downloader

import 'package:flutter_downloader/flutter_downloader.dart';
複製程式碼

文件中還提供了其他API,譬如暫停下載、取消下載,這裡就不再闡述了,文件已經寫的很清楚了。

到這其實就已經完成了下載的邏輯,然而下載的邏輯是實現了,想要讓使用者用的明白,我們還需要加一些提示資訊,就像開頭demo展示的有下載進度條和下載完成的提示框,接下來我們就來為下載設定這些提示資訊吧。

設定下載提示資訊

這裡以對話方塊和進度條的形式展現下載過程,我們使用到了progress_dialog這個外掛,可以很方便的顯示出一個下載對話方塊,地址是https://pub.flutter-io.cn/packages/progress_dialog。

使用progress_dialog外掛非常簡單,首先我們引入依賴檔案:

import 'package:progress_dialog/progress_dialog.dart';
複製程式碼

然後建立一個對話方塊:

ProgressDialog pr;
複製程式碼

如果想要建立一個下載提示對話方塊的話我們只需要在合適的地方初始化這個Dialog:

pr = new ProgressDialog(context,ProgressDialogType.Download);
複製程式碼

然後執行pr.show();即可顯示對話方塊。取消這個對話方塊也非常的簡單,只需執行pr.hide();

如果想要更新對話方塊中的提示資訊,比如下載進度,只需執行下述程式碼:

pr.update(progress: percentage,message: "Please wait...");
複製程式碼

同時我們還可以通過isShowing()函式判斷對話方塊是否顯示

bool isProgressDialogShowing = pr.isShowing();
複製程式碼

是不是非常方便呢?

有了展示的對話方塊,下一步自然就是獲取下載進度了,好在flutter_downloader已經給我們提供了一個下載回撥,我們可以在下面的這個回撥函式中更新我們的UI。

FlutterDownloader.registerCallback((id, status, progress) {
  // code to update your UI
});
複製程式碼

其中id是下載任務的id,status是當前id下載任務的狀態,有undefined,enqueued,running,complete,failed,canceled,paused這幾種狀態,progress便是當前id下載任務的進度。

這裡方便起見我選擇在initState()函式中初始化下載回撥函式和對話方塊:

@override
void initState() {
  super.initState();
  // 初始化進度條
  ProgressDialog pr = new ProgressDialog(context, ProgressDialogType.Download);
  pr.setMessage('下載中…');
  // 設定下載回撥
  FlutterDownloader.registerCallback((id, status, progress) {
  	// 列印輸出下載資訊
    print('Download task ($id) is in status ($status) and process ($progress)');
    ......
  });
複製程式碼

然後我們需要根據下載的狀態分情況討論

@override
void initState() {
  super.initState();
  ......
  // 設定下載回撥
  FlutterDownloader.registerCallback((id, status, progress) {
  	// 列印輸出下載資訊
    print('Download task ($id) is in status ($status) and process ($progress)');
    if (!pr.isShowing()) {
      pr.show();
    }
    if (status == DownloadTaskStatus.running) {
      pr.update(progress: progress.toDouble(), message: "下載中,請稍後…");
    }
    if (status == DownloadTaskStatus.failed) {
      showToast("下載異常,請稍後重試");
      if (pr.isShowing()) {
        pr.hide();
      }
    }
    if (status == DownloadTaskStatus.complete) {
      print(pr.isShowing());
      if (pr.isShowing()) {
        pr.hide();
      }
    }
  });


複製程式碼

其實到這裡下載檔案的操作就算結束了,但是通常在下載完成後APP都會提示你是否要開啟,於是在這我們乾脆 就擴充一下,實現開啟我們已經下載好的檔案。

開啟下載完成的檔案

那如何開啟已經下載好的檔案呢?外掛已經提供好了開啟下載檔案的API,我們只需要像下面這樣使用就可以了。

// 根據taskId開啟下載檔案
Future<bool> _openDownloadedFile(taskId) {
  return FlutterDownloader.open(taskId: taskId);
}
複製程式碼

想要開啟已經下載完成的檔案,我們必須要要確保檔案已經下載好了。所以我們需要緊接上面的程式碼中判斷下載完成的函式。這裡我們以彈出對話方塊的形式詢問使用者是否開啟檔案。

Flutter實現檔案下載

程式碼如下

@override
void initState() {
  super.initState();
		......
    if (status == DownloadTaskStatus.complete) {
      print(pr.isShowing());
      if (pr.isShowing()) {
        pr.hide();
      }
      // 顯示是否開啟的對話方塊
      showDialog(
          // 設定點選 dialog 外部不取消 dialog,預設能夠取消
          barrierDismissible: false,
          context: context,
          builder: (context) => AlertDialog(
                title: Text('提示'),
                // 標題文字樣式
                content: Text('檔案下載完成,是否開啟?'),
                // 內容文字樣式
                backgroundColor: CupertinoColors.white,
                elevation: 8.0,
                // 投影的陰影高度
                semanticLabel: 'Label',
                // 這個用於無障礙下彈出 dialog 的提示
                shape: Border.all(),
                // dialog 的操作按鈕,actions 的個數儘量控制不要過多,否則會溢位 `Overflow`
                actions: <Widget>[
                  // 點選取消按鈕
                  FlatButton(
                      onPressed: () => Navigator.pop(context),
                      child: Text('取消')),
                  // 點選開啟按鈕
                  FlatButton(
                      onPressed: () {
                        Navigator.pop(context);
                        // 開啟檔案
                        _openDownloadedFile(id).then((success) {
                          if (!success) {
                            Scaffold.of(context).showSnackBar(SnackBar(
                                content: Text('Cannot open this file')));
                          }
                        });
                      },
                      child: Text('開啟')),
                ],
              ));
    }
  });
}
複製程式碼

對話方塊的使用上述程式碼已經註釋的很詳細了。

至此,我們便使用 Flutter 完成了一個完整的下載檔案的過程了。

總結

總的來說,利用Flutter實現檔案下載的思路還是很清楚的,獲取許可權->獲取路徑->開始下載->監聽下載程式,一氣呵成。同時,藉助於 Flutter 社群的快速發展,已經有很多優秀的開發者開發了一些非常好用的外掛,憑藉著這些外掛我們可以快速實現自己想要的功能。在這個demo中整個介面編寫+邏輯實現總共也才 223 行程式碼,雖然介面有些醜陋,但考慮到Dart語言的迷之縮排這個行數也是很短的了。

最後想要原始碼可以掃描下面的二維碼關注我的公眾號「01二進位制」,後臺回覆「Flutter 檔案下載」即可,後期我也會不定時更新一些和Flutter有關的文章,希望大家可以多多支援。


Flutter實現檔案下載

相關文章