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?

More than 3 years have passed since last update.

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タグの外にあるこれはなんでしょうか、、、わかる方教えてください。。。
<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?