Імплементація завантаження файлів (Download) у мобільному застосунку
Завантаження файлів розділяється на два принципово різні сценарії: швидке завантаження невеликих ресурсів (зображення, документи до 10 МБ) прямо в пам'ять, та завантаження великих файлів (відео, архіви) з відображенням прогресу та можливістю відновлення. Змішування підходів — часта помилка, яка виражається в OOM-крахах або висінні індикатора без зворотного зв'язку.
Реалізація по платформах
Android. Для завантаження в пам'ять — OkHttp або Retrofit з ResponseBody.byteStream(), дані пишемо в файл в IO-корутині. Для великих файлів системний DownloadManager з уведомленням у статусбарі — користувач бачить прогрес навіть після виходу з застосунку. Альтернатива — WorkManager + кастомний Worker, якщо потрібно більше контролю над логікою.
Збереження в папку Downloads на Android 10+: використовуємо MediaStore API для загальнодоступних файлів, getExternalFilesDir() — для приватних. Спроба писати напрямку в /sdcard/Download/ без MediaStore на Android 11 дає SecurityException.
val request = DownloadManager.Request(Uri.parse(url))
.setTitle(fileName)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val downloadId = downloadManager.enqueue(request)
iOS. URLSession.downloadTask зберігає тимчасовий файл, після чого потрібно перемістити його в FileManager.default.urls(for: .documentDirectory). Для фонових завантажень — URLSessionConfiguration.background(withIdentifier:) з делегатом URLSessionDownloadDelegate. Без фонової сесії завантаження перериває при переході застосунку в background.
let config = URLSessionConfiguration.background(withIdentifier: "com.app.download")
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.downloadTask(with: URL(string: url)!)
task.resume()
Реалізація urlSession(_:downloadTask:didFinishDownloadingTo:) для переміщення файлу та urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:) для прогресу.
Flutter: пакет dio з onReceiveProgress, збереження через path_provider. Для фонового завантаження — flutter_downloader, який обгортає нативні DownloadManager (Android) та URLSession (iOS).
Нюанси, які часто пропускають
Файл може завантажитися частково через обрив. Resumable download через Range заголовок (Range: bytes=1048576-) працює лише якщо сервер повертає Accept-Ranges: bytes та Content-Range. Якщо сервер не підтримує — завантаження починається заново. Перед реалізацією resume перевіряємо поведінку бекенду.
Також важливо показувати реальний прогрес, а не детермінований. Якщо Content-Length у заголовку відсутній — прогрес-бар буде «крутитися» без відсотків.
Що входить у роботу
Виділяємо підхід під задачу (in-memory vs файл, foreground vs background), реалізуємо прогрес, збереження у правильну директорію, обробку помилок мережі та очистку тимчасових файлів.
Строк: 1–3 дні залежно від вимог до resumable та background-поведінки.







