How to Show Download Progress, Kotlin-Style - небольшая статья с хорошей иллюстрацией современных подходов к разработке на примере диалога загрузки файла.
Когда-то, на заре становления Android как самой популярной платформы, по сети гуляло множество примеров реализации подобной функциональности. Обычно все сводилось к использованию
Сегодня у нас есть мощные инструменты асинхронного программирования, позволяющие сделать этот код гораздо чище, понятнее и надежнее. И все это с использованием стандартных средств и библиотек разработки.
Итак, для начала создадим sealed-класс для управления состоянием загрузки:
Sealed-классы отличаются тем, что могут иметь только ограниченный и заранее определенный набор потомков. Нам нужно всего три состояния: 1. загрузка завершена, 2. ошибка загрузки, 3. текущий прогресс загрузки.
Теперь реализуем саму функцию загрузки:
Это функция-расширение для класса
Кроме того, что это функция-расширение, это еще и suspend-функция, которая возвращает Flow (поток данных), содержащий текущее состояние загрузки. Это значит, что код в теле функции будет выполнен не в момент вызова функции, а в момент получения данных из самого Flow (с помощью метода
Наконец, напишем функцию, которая будет запускать загрузку в фоновом потоке и динамически обновлять интерфейс приложения:
Вся логика в одной небольшой функции. Сначала запускаем фоновую корутину (
@ezobnin
Когда-то, на заре становления Android как самой популярной платформы, по сети гуляло множество примеров реализации подобной функциональности. Обычно все сводилось к использованию
AsyncTask и runOnUiThread(), а сам код выглядел как весьма далекая от паттерна MVC мешанина классов и методов.Сегодня у нас есть мощные инструменты асинхронного программирования, позволяющие сделать этот код гораздо чище, понятнее и надежнее. И все это с использованием стандартных средств и библиотек разработки.
Итак, для начала создадим sealed-класс для управления состоянием загрузки:
Код:
sealed class DownloadStatus {
object Success : DownloadStatus()
data class Error(val message: String) : DownloadStatus()
data class Progress(val progress: Int): DownloadStatus()
}
Теперь реализуем саму функцию загрузки:
Код:
suspend fun HttpClient.downloadFile(file: File, url: String): Flow<DownloadStatus> {
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 Koltin-библиотеки Ktor. Являясь расширением она может использовать методы класса HttpClient напрямую, к тому же нам нет необходимости создавать специальный утилитный класс для нее, в остальном коде она будет выглядеть как метод класса HttpClient.Кроме того, что это функция-расширение, это еще и suspend-функция, которая возвращает Flow (поток данных), содержащий текущее состояние загрузки. Это значит, что код в теле функции будет выполнен не в момент вызова функции, а в момент получения данных из самого Flow (с помощью метода
collect()). Это позволит сделать код более структурированным и понятным.Наконец, напишем функцию, которая будет запускать загрузку в фоновом потоке и динамически обновлять интерфейс приложения:
Код:
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(Dispatchers.IO).launch), затем запускаем загрузку файла и обновляем UI в соответствии с полученным из Flow состоянием.@ezobnin