How to Show Download Progress, Kotlin-Style — небольшая статья с хорошей иллюстрацией современных подходов к разработке на примере диалога загрузки файла.
Когда‑то, на заре становления Android как самой популярной платформы, по сети гуляло множество примеров реализации подобной функциональности. Обычно все сводилось к использованию AsyncTask и runOnUiThread(<wbr />), а сам код выглядел как весьма далекая от паттерна MVC мешанина классов и методов.
РЕКОМЕНДУЕМ:
Как сделать код на Kotlin более понятным
Сегодня у нас есть мощные инструменты асинхронного программирования, позволяющие сделать этот код гораздо чище, понятнее и надежнее. И все это с использованием стандартных средств и библиотек разработки.
Итак, для начала создадим sealed-класс для управления состоянием загрузки:
1 2 3 4 5 |
sealed class DownloadStatus { object Success : DownloadStatus() data class Error(val message: String) : DownloadStatus() data class Progress(val progress: Int): DownloadStatus() } |
Sealed-классы отличаются тем, что могут иметь только ограниченный и заранее определенный набор потомков. Нам нужно всего три состояния: загрузка завершена, ошибка загрузки, текущий прогресс загрузки.
Теперь реализуем саму функцию загрузки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
suspend fun HttpClient.downloadFile(file: File, url: String): Flow { return flow { val response = call { url(url) method = HttpMethod.Get }.response val byteArray = ByteArray(response.contentLength()!!.toInt()) var offset = 0 do { val currentRead = response.content.readAvailable(byteArray, offset, byteArray.size) offset += currentRead val progress = (offset * 100f / byteArray.size).roundToInt() emit(DownloadStatus.Progress(progress)) } while (currentRead > 0) response.close() if (response.status.isSuccess()) { file.writeBytes(byteArray) emit(DownloadStatus.Success) } else { emit(DownloadStatus.Error("File not downloaded")) } } } |
Это функция‑расширение для класса HttpClient Kotlin-библиотеки Ktor. Как расширение, она может использовать методы класса HttpClient напрямую, к тому же нам нет необходимости создавать специальный утилитный класс для нее, в остальном коде она будет выглядеть как метод класса HttpClient.
Кроме того что это функция‑расширение, это еще и suspend-функция, которая возвращает Flow (поток данных), содержащий текущее состояние загрузки. Это значит, что код в теле функции будет выполнен не в момент вызова функции, а в момент получения данных из самого Flow (с помощью метода collect(<wbr />)). Это позволит сделать код более структурированным и понятным.
РЕКОМЕНДУЕМ:
Функции-расширения в Kotlin: использовать или нет?
Наконец, напишем функцию, которая будет запускать загрузку в фоновом потоке и динамически обновлять интерфейс приложения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
private fun downloadWithFlow() { CoroutineScope(Dispatchers.IO).launch { ktor.downloadFile(file, url).collect { withContext(Dispatchers.Main) { when (it) { is DownloadStatus.Success -> { // Обновляем UI } is DownloadStatus.Error -> { // Обновляем UI } is DownloadStatus.Progress -> { // Обновляем UI } } } } } } |
Вся логика в одной небольшой функции. Сначала запускаем фоновую корутину ( CoroutineScope(<wbr />Dispatchers.<wbr />IO).<wbr />launch), затем запускаем загрузку файла и обновляем UI в соответствии с полученным из Flow состоянием.