2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2021-09-23

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

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

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

Githubのレポジトリーはこちら

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),
          );
        },
      ))),
    );
  }
}

実装はこんな感じ
download_qiita.gif

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?