複数ファイルのダウンロードの進捗をリアルタイムで表示する
今回は動画ファイルをダウンロードしていきます。
色々ググっても複数ファイルのダウンロード進捗をリアルタイムで更新する実装を見かけなかったので、僕なりに実装したものを晒します。
ダウンロード中にアプリをキルしてしまった場合、前回のダウンロードが完了したファイルについては飛ばしてダウンロードされるように実装しています。
※殴り書きなので、ディレクトリ構造や状態管理はほとんど考慮していません
※とりあえず動くものを!をモットーにやってます。汚いコーディングだと思います。読みにくければすみません。。。
1.使用パッケージの導入
pubspec.yamlに以下を追記(バージョンは最新のものを使ってください。)
pubspec.yaml
dependencies:
flutter:
sdk: flutter
path_provider: ^2.0.2 //追記
http: ^0.13.3 //追記
rxdart: ^0.27.1 //追記
2.OS毎の設定
Android
android/app/src/main/AndroidManifest.xmlに追記
<application
android:requestLegacyExternalStorage="true" //追記
android:label="download_sample"
android:icon="@mipmap/ic_launcher">
iOS
ios/Runner/info.plistに追記
<key>NSAppTransportSecurity</key> //追記
<dict>
<key>NSAllowsArbitraryLoads</key> //追記
<true/> //追記
</dict>
3.動画ダウンロード処理について
video_data.dart
まず、ダウンロードする動画の保存するファイル名とダウンロード元のURLを持つクラスを作ります。
lib/entity/video_data.dart
import 'package:flutter/material.dart';
@immutable
class VideoData {
const VideoData(this.videoUrl, this.videoName);
final String videoUrl;
final String videoName;
}
List<VideoData> videoDataList = [
VideoData(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
'b.mp4'),
VideoData(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
'c.mp4'),
VideoData(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
'd.mp4'),
VideoData(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
'e.mp4'),
VideoData(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
'bf.mp4'),
VideoData(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
'bg.mp4'),
VideoData(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
'bh.mp4'),
VideoData(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
'bi.mp4'),
];
download_sample_model.dart
ここではMVVMでModelに当たる処理を書いています。
lib/ui/download_sample/download_sample_model.dart
import 'dart:async';
import 'dart:io';
import 'package:download_sample/entity/video_data.dart';
import 'package:path_provider/path_provider.dart';
import 'package:http/http.dart' as http;
class DownloadSampleModel {
Future<void> downloadVideos(
StreamController<double> stream, //viewModelで returnするためのstreamController
List<VideoData> downloadVideoDataList,
) async {
//アプリのディレクトリのpathをここで取得
final _directory = await getApplicationDocumentsDirectory();
String _localPath = _directory.path;
//ダウンロードの途中だった場合、Directoryはすでに存在しているので作る必要がないため
if (await Directory('$_localPath/videos').exists() == false) {
//rootのディレクトリの配下にディレクトリを新たに作る
var _newDirectory = Directory('$_localPath/videos');
await _newDirectory.create(recursive: true);
}
double _progress = 0;
final List<double> _progressList = [];
for (int i = 0; i < downloadVideoDataList.length; i++) {
//urlは動画が置いてあるurl,fileNameは保存する動画のファイル名(拡張子も含める),directoryは動画を保存したdirectory名
var _req =
http.Request('GET', Uri.parse(downloadVideoDataList[i].videoUrl));
final http.StreamedResponse response =
await http.Client().send(_req).timeout(Duration(seconds: 10));
final _contentLength = response.contentLength;
//$_localPath/はダウンロードするファイルを保存する場所のpath
final File downloadFile =
File('$_localPath/videos/${downloadVideoDataList[i].videoName}');
//i番目の動画がすでにダウンロード済かどうか確認
final bool _isDownloaded = await downloadFile.exists();
//ダウンロード済だった場合
if (_isDownloaded == true &&
//_isDownloadedだけではダウンロード済みでも前回のダウンロードが完全でなかった場合、動画として不完全な物になってしまうため、データ量で判定
downloadFile.lengthSync() == _contentLength) {
_progress = 0;
_progressList.add(0);
_progressList[i] = 100;
_progressList.forEach((element) {
_progress = _progress + element / downloadVideoDataList.length;
});
stream.sink.add(_progress);
}
//ダウンロード済でなかった場合
else {
List<int> bytes = [];
//i番目のダウンロードの進捗情報を追加
_progressList.add(0);
try {
response.stream.listen(
(List<int> newBytes) {
bytes.addAll(newBytes);
final downloadLength = bytes.length;
_progressList[i] = 100 * downloadLength / _contentLength!;
_progress = 0;
_progressList.forEach((element) {
_progress = _progress + element / downloadVideoDataList.length;
});
stream.sink.add(_progress);
},
onDone: () async {
await downloadFile.writeAsBytes(bytes);
},
);
} catch (e) {
print(e);
}
}
}
}
}
download_sample_view_model.dart
MVVMでいうViewModelに当たる記述です。
ここでStreamを返して、viewはStreamBuilderでストリームで流れてくるdoubleを受け取って、リアルタイム更新します。
lib/ui/download_sample/download_sample_view_model.dart
import 'package:download_sample/entity/video_data.dart';
import 'package:download_sample/ui/download_sample/download_sample_model.dart';
import 'package:rxdart/rxdart.dart';
class DownloadSampleViewModel {
Stream<double> downloadVideos(List<VideoData> downloadVideoDataList) {
final streamController = BehaviorSubject<double>();
DownloadSampleModel()
.downloadVideos(streamController, downloadVideoDataList)
.onError((error, stackTrace) => print(error));
return streamController.stream;
}
}
download_sample_page.dart
ui/download_sample/download_sample_page.dart
import 'package:download_sample/entity/video_data.dart';
import 'package:download_sample/ui/download_sample/download_sample_view_model.dart';
import 'package:flutter/material.dart';
class DownloadSamplePage extends StatelessWidget {
final viewModel = DownloadSampleViewModel();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: StreamBuilder(
stream: viewModel.downloadVideos(videoDataList),
builder: (context, snapshot) {
if(!snapshot.hasData) {
return CircularProgressIndicator();
}
return Text(
snapshot.data.toString() + '%',
style: TextStyle(fontSize: 20),
);
},
))),
);
}
}