Асинхронное программирование в Kotlin: полное руководство на практике
Современные приложения требуют высокой производительности и отзывчивости. Асинхронное программирование в Kotlin с помощью корутин (coroutines) стало стандартом де-факто для работы с сетевыми запросами, базами данных и файловыми операциями. В этом руководстве мы разберем основные концепции, покажем практические примеры и дадим готовые шаблоны для использования в ваших проектах.
1. Основы корутин в Kotlin
Корутины — это легковесные потоки, которые позволяют приостанавливать выполнение функции без блокировки основного потока. В отличие от традиционных потоков, корутины могут быть приостановлены в любой момент и возобновлены позже, что делает их идеальными для асинхронных операций.
1.1. Подключение зависимостей
Для начала добавьте зависимости в build.gradle.kts:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0") // для Android
}
1.2. Первая корутина
Создадим простую корутину с помощью runBlocking — моста между синхронным и асинхронным миром:
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L) // неблокирующая задержка
println("Мир!")
}
println("Привет,")
delay(2000L) // ждём завершения корутины
}
В этом примере launch запускает новую корутину, а delay — это приостанавливающая функция (suspend), которая не блокирует поток.
2. Suspend функции и структурированный параллелизм
2.1. Создание suspend функций
Любая функция, которая может быть приостановлена, должна быть помечена модификатором suspend. Такие функции можно вызывать только из корутин или других suspend функций:
suspend fun fetchUserData(userId: String): User {
delay(2000L) // имитация сетевого запроса
return User(userId, "Иван Петров")
}
suspend fun fetchUserPosts(userId: String): List<Post> {
delay(1500L)
return listOf(Post("Заголовок 1"), Post("Заголовок 2"))
}
2.2. Последовательное выполнение
По умолчанию корутины выполняются последовательно внутри одного launch:
fun main() = runBlocking {
val time = measureTimeMillis {
val user = fetchUserData("123")
val posts = fetchUserPosts("123")
println("${user.name} имеет ${posts.size} постов")
}
println("Время выполнения: $time мс") // ~3500 мс
}
2.3. Параллельное выполнение с async/await
Для параллельного выполнения используйте async и await:
fun main() = runBlocking {
val time = measureTimeMillis {
val user = async { fetchUserData("123") }
val posts = async { fetchUserPosts("123") }
println("${user.await().name} имеет ${posts.await().size} постов")
}
println("Время выполнения: $time мс") // ~2000 мс (параллельно)
}
3. Диспетчеры корутин и контексты
Диспетчеры определяют, на каком потоке или пуле потоков будет выполняться корутина. Основные диспетчеры:
- Dispatchers.Default — для CPU-интенсивных задач (по умолчанию)
- Dispatchers.IO — для операций ввода-вывода (сеть, файлы)
- Dispatchers.Main — для UI-потока (Android, Swing)
- Dispatchers.Unconfined — не привязан к конкретному потоку
3.1. Пример с Dispatchers.IO
fun main() = runBlocking {
launch(Dispatchers.IO) {
// Сетевой запрос
val response = httpClient.get("https://api.example.com/data")
withContext(Dispatchers.Main) {
// Обновление UI
textView.text = response.body()
}
}
}
3.2. withContext для переключения контекста
Функция withContext позволяет временно сменить диспетчер внутри корутины:
suspend fun loadData(): String = withContext(Dispatchers.IO) {
// Тяжелая операция в фоне
Thread.sleep(1000)
"Данные загружены"
}
4. Flow — реактивные потоки данных
Flow — это асинхронный аналог Sequence, который эмитирует несколько значений и поддерживает холодные потоки данных. Идеально подходит для подписок на обновления, прогресс-баров и стримов данных.
4.1. Создание простого Flow
import kotlinx.coroutines.flow.*
fun simpleFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(1000L)
emit(i) // эмитируем значение
}
}
fun main() = runBlocking {
simpleFlow().collect { value ->
println("Получено: $value")
}
}
4.2. Трансформация Flow
Flow поддерживает все операторы, знакомые по RxJava/Kotlin Sequences:
fun main() = runBlocking {
simpleFlow()
.map { it * 2 }
.filter { it % 3 == 0 }
.catch { e -> println("Ошибка: $e") }
.collect { println("Результат: $it") }
}
4.3. StateFlow и SharedFlow
Для управления состоянием используйте StateFlow (хранит последнее значение) и SharedFlow (для широковещательных событий):
class ViewModel {
private val _state = MutableStateFlow("Начальное состояние")
val state: StateFlow<String> = _state.asStateFlow()
fun update() {
viewModelScope.launch {
_state.value = "Новое состояние"
}
}
}