以前有作一個工具集的微信小程序「開掛Lite」,可是因爲小程序自身限制,沒有辦法實現下載文件的功能,只能把下載連接解析出來。並且受限於微信平臺,小程序的審覈是一件很麻煩的事情,所以有了將其APP化的想法。android
自從去年Flutter橫空出世後,我便一直關注它的發展,時隔一年後從新拾起,發現它的生態已經初具規模,因而決定採用Flutter重作一個「開掛Lite」。後期我也會不定時更新一些和Flutter有關的文章,但願你們能夠多多支持。本文記錄的即是我利用Flutter實現文件下載功能的過程。sql
完整源碼可在公衆號:「01二進制」後臺回覆:「Flutter 文件下載」獲取小程序
咱們先看一下實現的效果:微信小程序
iOS安全
Android微信
本demo的實現效果很是簡單,就是點擊一個按鈕,而後下載文件,完成後提示用戶是否打開文件。網絡
在本 demo 中使用的 IDE 爲 Android Studio,同時使用到了如下幾個庫:session
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
文件,添加位置以下:app
接下來咱們能夠在 Terminal 中輸入flutter packages get
或者點擊 IDE 左上角的Packages get
字樣安裝依賴。async
而後將初始項目中的多餘代碼刪除,並在中間添加一個按鈕。
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: Text("點我下載文件"),
onPressed: () {
// 執行下載操做
_doDownloadOperation();
},
),
],
),
),
複製代碼
其中_doDownloadOperation()
即是咱們執行下載操做的方法,至此,前期準備工做結束。
雖然整個下載演示的過程很是簡單,但仍是有必要來分析整個下載的流程,以下圖所示:
因此咱們接下來要作的事情即是:
這裏使用到一個權限獲取插件:permission_handler
,這個插件提供了跨平臺(Android和iOS)的權限檢查以及獲取API,地址在:pub.flutter-io.cn/packages/pe…
打開項目根目錄下的android/app/src/main/AndroidManifest.xml
文件,位置以下圖所示:
而後添加咱們須要使用的權限的申明,以下圖所示:
接下來咱們就能夠寫代碼來獲取所需的權限了。建立一個_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端配置
想要執行這一步,咱們在Xcode中打開該項目的 iOS module,以下圖所示:
而後雙擊左側Runner選項,選擇 Capabilities 選項,按圖中所示啓用background mode
文檔中還提供了一些可選配置:
爲了安全起見,蘋果官方已經默認不讓開發者使用不安全的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>
複製代碼
位置以下:
還有其餘相似於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);
}
複製代碼
想要打開已經下載完成的文件,咱們必需要要確保文件已經下載好了。因此咱們須要緊接上面的代碼中判斷下載完成的函數。這裏咱們以彈出對話框的形式詢問用戶是否打開文件。
代碼以下
@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有關的文章,但願你們能夠多多支持。