Реалізація завантаження файлів (Download) у мобільному додатку
Завантаження файлів — не просто виклик API. Розділяйте два принципово різні сценарії: швидке завантаження невеликих ресурсів (зображення, документи до 10 МБ) прямо в пам'яті, та завантаження великих файлів (відео, архіви до 10 ГБ) з відображенням прогресу та можливістю відновити. Змішувати підходи — часта помилка, яка призводить до OOM-кришів або індикатора, що крутиться без зворотного зв'язку. Ми реалізували цю логіку в 50+ проєктах за 10 років на ринку — гарантуємо стабільну роботу навіть на слабких пристроях.
Як уникнути OOM при завантаженні великих файлів?
Основна причина OOM — спроба завантажити весь файл у пам'ять. Для файлів >10 МБ завжди використовуйте потоковий запис на диск. На Android — OkHttp з ResponseBody.byteStream() і запис у файл через FileOutputStream у фоновій корутині. На iOS — URLSession.downloadTask зберігає у тимчасовий файл автоматично. Для Flutter — dio з опцією receiveTimeout і запис через File. На практиці OOM виникає при завантаженні файлів >50 МБ без буферизації.
Чому прогрес-бар може не працювати?
Прогрес-бар показує коректні відсотки лише якщо сервер повертає заголовок Content-Length. Якщо його нема — індикатор буде «крутитися» без числового значення. У таких випадках можна показувати невизначений прогрес (activity indicator) або завантажувати файл частинами через Range-запити, якщо сервер підтримує докачування. Більше 30% серверів не віддають Content-Length, тому тестуйте на реальному бекенді.
Реалізація за платформами
Порівняємо підходи на трьох основних платформах:
| Платформа | Маленькі файли | Великі файли | Resume | Background |
|---|---|---|---|---|
| Android | OkHttp/Retrofit | DownloadManager або WorkManager | DownloadManager (частково) | DownloadManager / WorkManager |
| iOS | URLSession.dataTask | URLSession.downloadTask (background) | Ручна реалізація через URLSession | URLSession background configuration |
| Flutter | Dio | flutter_downloader | Dio (ручна) | flutter_downloader |
Android. Для завантаження в пам'ять — OkHttp або Retrofit з ResponseBody.byteStream(), дані пишемо у файл в IO-корутині. Для великих файлів системний DownloadManager з повідомленням у статусбарі — користувач бачить прогрес навіть після виходу з додатка. Альтернатива — WorkManager + кастомний Worker, якщо потрібно більше контролю над логікою.
Збереження в папку Downloads на Android 10+: використовуємо MediaStore API для загальнодоступних файлів, getExternalFilesDir() — для приватних. Спроба писати напряму в /sdcard/Download/ без MediaStore на сучасних версіях викличе 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 варто перевірити поведінку бекенду — це економить до 30% часу завантаження при нестабільному з'єднанні.
Також важливо показувати реальний прогрес, а не детермінований. Якщо Content-Length у заголовку відсутній — прогрес-бар буде «крутитися» без відсотків. У цьому випадку краще використовувати невизначений індикатор.
Процес роботи
- Аналіз вимог: які файли, максимальний розмір, чи потрібен resume та фонове завантаження.
- Вибір стеку:
DownloadManagerабоURLSessionbackground з конфігурацією. - Проєктування: схема збереження, обробка помилок, діалог прогресу.
- Реалізація: написання коду з урахуванням платформених особливостей (ProGuard/R8, App Transport Security).
- Тестування: на реальних пристроях з різною швидкістю мережі та перериваннями.
- Деплой: публікація в App Store та Google Play з відлагодженням crash logs.
Типові помилки при завантаженні файлів
- Завантаження в main thread — гарантований ANR на Android.
- Ігнорування
Content-Type— файл може зберегтися без розширення. - Не очищуються тимчасові файли — зростає
Documents & Data. - На iOS не знімається
URLSessionпісля фонової задачі — витік пам'яті.
Що входить у роботу
Обираємо підхід під задачу (in-memory vs файл, foreground vs background), реалізуємо прогрес, збереження в потрібну директорію, обробку помилок мережі та очищення тимчасових файлів. Ми під ключ реалізуємо завантаження з урахуванням усіх платформених особливостей — від App Store Review Guidelines до ProGuard правил.
Термін: 1–3 дні залежно від вимог до resumable та background-поведінки. Зв'яжіться з нами, щоб оцінити ваш проєкт — пишіть, ми запропонуємо оптимальне рішення з урахуванням вашого стеку. Замовте консультацію, і ми розберемо ваш кейс.
Документація: URLSession Programming Guide, DownloadManager Reference







