Вращающийся индикатор прогресса - это нормально, когда вам нужно подождать всего пару секунд, но когда загрузка занимает намного больше времени, пользователи начинают думать, что ваше приложение не работает.

Если вы дадите им понять, сколько еще им придется ждать, они проявят немного больше терпения.

В этой статье я покажу вам, как загрузить файл и отслеживать прогресс с помощью CircularProgressIndicator:

И как только вы это сделаете, его так же легко заменить на LinearProgressIndicator:

Виджеты индикатора прогресса

Вы, конечно, можете создать свой собственный индикатор прогресса, но Flutter поставляется с двумя встроенными виджетами индикатора прогресса, как показано в анимированных гифках, которые я вам показал выше.

Если вы хотите узнать о них больше, посмотрите это короткое видео:

Сначала мы изготовим CircularProgressIndicator, а потом, если хотите, вы сможете обменять его на LinearProgressIndicator. Код почти такой же.

Круговой индикатор прогресса

Добавьте индикатор выполнения в макет пользовательского интерфейса.

Center(
  child: SizedBox(
    width: 100,
    height: 100,
    child: CircularProgressIndicator(
      strokeWidth: 20,
      value: model.downloadProgress,
    ),
  ),
),

Включение параметра value - это то, что делает его определенным индикатором прогресса, то есть то, что заставляет индикатор показывать завершенную часть. Без него (или когда value равно null) он становится неопределенным индикатором прогресса и просто продолжает вращаться вечно.

Значение должно быть double между 0 и 1. Если он окажется выше 1, он просто покажет полный круг, как 1.

Обратите внимание, что я получаю информацию о ходе загрузки с model.downloadProgress. Я использую ChangeNotifier в качестве модели представления и предоставляю ее дереву виджетов с Provider библиотекой. Это архитектурный паттерн, который я описал ранее:



Конечно, вы можете использовать любой архитектурный паттерн, который вам нравится. Это не повлияет на работу логики загрузки. В этом разделе вам нужно только включить виджет индикатора выполнения и указать способ обновления value.

Я включу свой полный код в конце этой статьи.

Кнопка для запуска загрузки

Также добавьте кнопку, чтобы модель представления начала загрузку файла.

RaisedButton(
  child: Text('Download file'),
  onPressed: () {
    model.startDownloading();
  },
),

Логика загрузки

Внутри модели представления вы реализуете логику, которую вы вызвали в макете виджета:

class MyViewModel extends ChangeNotifier {
  double _progress = 0;
  get downloadProgress => _progress;

  void startDownloading() async {
    // ...
  }

}

Когда _progress равно 0, индикатор выполнения становится невидимым. Вы можете сделать что-то еще в производственном приложении, но в этом примере это хорошо работает.

Вращать при подключении

При первом вызове startDownloading() требуется немного времени для связи с сервером. Пока это происходит, вы можете заставить круг просто делать свое вечное вращение:

void startDownloading() async {
  _progress = null;
  notifyListeners();
  // ...
}

Помните, что создание значения CircularProgressIndicator null - это то, что заставляет его вращаться.

Выберите файл для загрузки

Я просто использую случайный mp3-файл размером 5 МБ из Интернета. С таким же успехом это может быть изображение, PDF или APK.

final url = 'https://file-examples.com/wp-content/uploads/2017/11/file_example_MP3_5MG.mp3';

Сделать запрос GET

Чтобы сделать запрос GET, я использую http пакет от команды Dart. Вы можете использовать dio, но я предпочитаю работать на более низком уровне и избегать сторонних пакетов, когда они мне не нужны. Пакет http поддерживает как Интернет, так и нативный.

final request = Request('GET', Uri.parse(url));
final StreamedResponse response = await Client().send(request);

Вы можете спросить, почему я использовал Client().send() вместо более распространенного метода get(url). Причина в том, что send() дает вам поток, и вы собираетесь прослушивать поток байтов, когда он загружает файл с сервера.

Получить длину контента

Ответ, приходящий от сервера, должен содержать заголовок под названием Content-Length, который включает общий размер файла в байтах. Вы можете получить это значение так:

final contentLength = response.contentLength;

Иногда сервер не возвращает это значение, а иногда удаляется заголовок. В этом случае contentLength будет нулевым. Это усложняет демонстрацию пользователям прогресса загрузки. Есть несколько вариантов:

  • Если у вас есть контроль над сервером, вы можете установить заголовок x-decompressed-content-length с размером файла перед его отправкой. Этот заголовок, кажется, остался на месте. На стороне клиента вы можете получить длину контента следующим образом:
final contentLength = double.parse(response.headers['x-decompressed-content-length']);
  • Другой вариант - просто показать совокупное количество загружаемых байтов. Поскольку окончательная сумма неизвестна, пользователь все равно не знает, сколько времени ему нужно ждать, но, по крайней мере, это будет более информативно, чем вечный вращающийся круг.

Но в любом случае для url, который я использую в этом примере, response.contentLength работает нормально, поэтому нам не о чем беспокоиться.

Начать загрузку

Теперь, когда у нас есть ответ от сервера, мы можем остановить вращение индикатора и установить его на 0.

_progress = 0;
notifyListeners();

Мы также инициализируем переменную для сохранения загрузки.

List<int> bytes = [];

Массив bytes сохраняет файл в памяти перед сохранением в хранилище. Поскольку длина этого массива - это количество байтов, которые были загружены, вы сможете использовать его для отслеживания хода загрузки.

Выберите место для хранения файла

Я просто назову файл song.mp3:

final file = await _getFile('song.mp3');

Метод _getFile() находит подходящее место на устройстве пользователя для размещения файла.

Future<File> _getFile(String filename) async {
  final dir = await getApplicationDocumentsDirectory();
  return File("${dir.path}/$filename");
}

В этом случае мы выбираем каталог документов. Вы также можете выбрать временный каталог или еще что-нибудь. Этот метод использует пакет path_provider для получения этого местоположения.

Обратите внимание, что здесь мы делаем выбор в отношении нашего приложения Flutter. Он может работать только на Android или iOS из-за path_provider и File (от dart:io). Если вы хотите, чтобы это работало в Интернете, просмотрите этот вопрос и подумайте об использовании пакета universal_html.

Слушайте входящий поток байтов

Поскольку ответом является StreamedResponse, вы можете слушать его, как любой Stream.

response.stream.listen(
  (List<int> newBytes) {
    // update progress
  },
  onDone: () async {
    // save file
  },
  onError: (e) {
    print(e);
  },
  cancelOnError: true,
);

Обновить прогресс

В обратном вызове потока обновите прогресс из списка входящих байтов.

// update progress
bytes.addAll(newBytes);
final downloadedLength = bytes.length;
_progress = downloadedLength / contentLength;
notifyListeners();

Вы используете эти новые байты для обновления массива bytes файла в памяти, а также количества загруженных байтов на данный момент. Поскольку вы также знаете общее количество байтов, вы можете рассчитать долю завершенных. Вызовите notifyListeners(), чтобы перестроить пользовательский интерфейс, чтобы CircularProgressIndicator отображал новое значение.

Сохраните файл по завершении

Обратный вызов onDone вызывается после завершения загрузки файла:

// save file
_progress = 0;
notifyListeners();
await file.writeAsBytes(bytes);

Установка прогресса на 0 приведет к исчезновению CircularProgressIndicator. После этого (или раньше, если хотите) вы можете записать байты в файл, который вы указали ранее.

Обработка ошибок

Я мало что сделал здесь, но если ваш пользователь потеряет подключение к Интернету при загрузке файла, это вызовет ошибку. Вы можете решить, что с этим делать, в onError. Установка cancelOnError на true приведет к отмене StreamSubscription.

Законченный

Это все. Теперь вы можете показать пользователям, как идет загрузка файла.

Спасибо коду здесь и здесь за то, что помогли мне.

Полный код

Вот модель представления:

А вот макет UI:

И зависимости:

Https://www.twitter.com/FlutterComm