Flutter/複数ファイルのダウンロードの進捗をリアルタイムで表示

# 複数ファイルのダウンロードの進捗をリアルタイムで表示する
今回は動画ファイルをダウンロードしていきます。
色々ググっても複数ファイルのダウンロード進捗をリアルタイムで更新する実装を見かけなかったので、僕なりに実装したものを晒します。
ダウンロード中にアプリをキルしてしまった場合、前回のダウンロードが完了したファイルについては飛ばしてダウンロードされるように実装しています。

※殴り書きなので、ディレクトリ構造や状態管理はほとんど考慮していません
※とりあえず動くものを！をモットーにやってます。汚いコーディングだと思います。読みにくければすみません。。。

[Githubのレポジトリーはこちら](https://github.com/489yuki/download_sample)


## 1.使用パッケージの導入

https://pub.dev/packages/path_provider

https://pub.dev/packages/http

https://pub.dev/packages/rxdart

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<double>を返して、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),
          );
        },
      ))),
    );
  }
}

```

実装はこんな感じ
![download_qiita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/929529/92f355f7-d2df-7247-d455-c4c5a656b3af.gif)