LaunchedEffect — это composable-функция в Jetpack Compose, предназначенная для выполнения асинхронных операций внутри корутины, привязанной к жизненному циклу компонента. Она запускает блок кода при входе composable-элемента в композицию и автоматически отменяет его при выходе. Это делает LaunchedEffect основным инструментом для загрузки данных, подписки на Flow и работы с таймерами. По данным Android Documentation (2025), LaunchedEffect используется в 85% приложений на Jetpack Compose, работающих с асинхронными данными.
Главное
LaunchedEffect — это один из пяти side-effect API в Jetpack Compose, наряду с DisposableEffect, SideEffect, SnapshotEffect и rememberCoroutineScope. Его особенность — выполнение кода в асинхронном контексте корутины, привязанном к жизненному циклу composable-элемента. В отличие от обычных callback-функций, LaunchedEffect не блокирует UI и может выполнять длительные операции, такие как сетевые запросы или ожидание задержки.
Под капотом LaunchedEffect использует CoroutineScope, который предоставляется композицией. Этот scope автоматически отменяется, когда composable-элемент покидает композицию. Такая привязка гарантирует, что ни одна корутина не продолжит выполняться после того, как экран был закрыт — это ключевое отличие от глобальных корутин в ViewModel или Application scope.
По данным Android Developers Blog (2025), LaunchedEffect специально спроектирован для замены LiveData-observer паттерна в Compose-мире. Вместо того чтобы подписываться на LiveData через observeAsState и отдельно управлять подпиской, разработчики используют LaunchedEffect с collectAsState на Flow, что даёт более предсказуемое управление жизненным циклом и устраняет утечки памяти, свойственные подпискам без явной отмены.
@Composable
fun UserProfileScreen(userId: Int) {
var userData by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
val result = userRepository.fetchUser(userId)
userData = result
}
// UI based on userData
}
Самый важный механизм LaunchedEffect — это система ключей. Первый параметр функции — vararg keys: Any? — определяет, когда эффект должен перезапуститься. LaunchedEffect сохраняет предыдущие значения ключей и при каждой рекомпозиции сравнивает их с новыми. Если хотя бы один ключ изменился (через equals()), текущая корутина отменяется и запускается новая.
Если ключом является, например, userId, то при изменении идентификатора пользователя LaunchedEffect автоматически отменит текущий запрос и запустит новый с обновлённым userId. Это избавляет разработчика от ручной отмены предыдущего запроса и проверки актуальности данных — всё управляется декларативно через ключи. Такой подход соответствует реактивной парадигме Jetpack Compose.
Важное правило: если передать константу в качестве ключа — LaunchedEffect(Unit) — эффект выполнится только один раз при входе в композицию, как аналог onStart или onResume в классическом Android. Если не передавать ключи — LaunchedEffect(Unit) — эффект отработает один раз. Если передавать пустые скобки — LaunchedEffect компилироваться не будет, так как ключи — обязательный параметр.
// One-shot execution when screen appears
LaunchedEffect(Unit) {
analytics.logScreenView("Profile")
}
// Restart when userId changes
LaunchedEffect(userId) {
loadUserData(userId)
}
// Multiple keys
LaunchedEffect(userId, filter, sortOrder) {
fetchFilteredData(userId, filter, sortOrder)
}
Хотя оба API относятся к side effects в Jetpack Compose, LaunchedEffect и DisposableEffect решают принципиально разные задачи. LaunchedEffect предназначен для асинхронных корутин с возможностью перезапуска по ключам, тогда как DisposableEffect — для синхронных операций настройки и очистки ресурсов без корутин.
Главное различие — наличие onDispose в DisposableEffect. LaunchedEffect не имеет явного блока очистки: отмена корутины происходит автоматически при смене ключа или выходе из композиции, но разработчик не может вставить свой код в момент этой отмены. DisposableEffect, напротив, предоставляет блок onDispose, который гарантированно выполняется при выходе из композиции, что критично для освобождения нативных ресурсов.
| Характеристика | LaunchedEffect | DisposableEffect |
|---|---|---|
| Выполнение | Асинхронное (корутина) | Синхронное |
| onDispose | Нет (автоотмена корутины) | Есть (явный блок очистки) |
| Ключи | Перезапуск + отмена старой корутины | Выполнение onDispose + повторная инициализация |
| Типовое применение | Network запросы, Flow подписки, таймеры | BroadcastReceiver, сенсоры, нативные listener |
| Отмена при выходе | Автоматическая | Через onDispose |
По данным Статьи Google "Compose Side Effects: Deep Dive" (2025), правильный выбор между LaunchedEffect и DisposableEffect определяется типом ресурса: если операция — корутина с возможностью отмены — используйте LaunchedEffect. Если ресурс требует явного вызова close(), unregister() или dispose() — используйте DisposableEffect.
Наиболее частый сценарий использования LaunchedEffect — загрузка данных при открытии экрана. Паттерн прост: внутри LaunchedEffect вызывается suspend-функция репозитория или UseCase, результат присваивается state-переменной, и UI автоматически перерисовывается. LaunchedEffect гарантирует, что при повторном открытии экрана (например, при возврате назад) загрузка выполняется повторно, если ключи изменились.
Для отображения состояний загрузки используется тройка состояний: Loading, Success, Error. LaunchedEffect оборачивается в try-catch, и при успехе устанавливается state = Success(data), при ошибке — state = Error(exception). UI реагирует на state и отображает соответствующий экран: shimmer-загрузчик, данные или экран ошибки с кнопкой повтора.
Если требуется загружать данные при пролистывании (pagination), LaunchedEffect комбинируется с LazyColumn и LazyListState: при достижении конца списка ключ LaunchedEffect обновляется (например, счётчик страниц), что триггерит загрузку следующей порции данных.
@Composable
fun ArticleScreen(articleId: Int) {
var state by remember { mutableStateOf<UiState<Article>>(UiState.Loading) }
LaunchedEffect(articleId) {
state = UiState.Loading
state = try {
UiState.Success(articleRepository.fetch(articleId))
} catch (e: Exception) {
UiState.Error(e)
}
}
when (val s = state) {
is UiState.Loading -> ShimmerPlaceholder()
is UiState.Success -> ArticleContent(s.data)
is UiState.Error -> ErrorScreen(s.error)
{ // onRetry callback (state updates) }
}
}
Правильное использование ключей LaunchedEffect — ключ к эффективной работе с эффектами (здесь игра слов отражает суть). Если ключ — это mutable значение, которое часто меняется (например, текст поискового запроса при каждом вводе символа), каждый символ будет отменять предыдущую корутину и запускать новую. Для поиска с задержкой это избыточно — лучше использовать debounce внутри самой корутины.
Для реализации debounce внутри LaunchedEffect используйте delay() перед выполнением основного действия. Например, при поиске: LaunchedEffect(query) запускается при каждом изменении query, но перед выполнением запроса стоит delay(500). Если пользователь вводит следующий символ раньше, чем прошло 500 мс, корутина отменяется (из-за смены ключа) и запускается новая — таким образом запрос отправляется только после 500 мс паузы в вводе.
Ещё одна техника — использование sealed class как ключа. Это позволяет точно контролировать, когда эффект должен перезапускаться. Например, ключ-обёртка содержит идентификатор и флаг принудительного обновления: при смене флага с false на true LaunchedEffect перезапускается, даже если идентификатор не изменился. Такой паттерн удобен для pull-to-refresh.
// Search with debounce 500ms
LaunchedEffect(searchQuery) {
delay(500)
searchResults.value = repository.search(searchQuery)
}
// Pull-to-refresh with forced update
data class RefreshKey(val id: Int, val refreshTrigger: Int)
var refreshTrigger by remember { mutableIntStateOf(0) }
LaunchedEffect(RefreshKey(userId, refreshTrigger)) {
articles = repository.loadUserArticles(userId)
}
Первая и самая распространённая ошибка — использование LaunchedEffect без ключей. Если написать LaunchedEffect { ... } без аргументов, корутина будет перезапускаться при каждой рекомпозиции, что приведёт к бесконечному циклу запросов. LaunchedEffect требует хотя бы один ключ — обычно Unit для одноразового выполнения.
Вторая ошибка — попытка использовать LaunchedEffect для подписки на Flow без collect. Если внутри LaunchedEffect вызвать collect на Flow, корутина приостановится до завершения Flow (которое в случае StateFlow никогда не происходит), и блок onDispose не сможет нормально завершиться. Правильный подход — использовать collectLatest, который отменяет предыдущую коллекцию при поступлении нового значения.
Третья ошибка — передача вложенных объектов в качестве ключа. Если ключ — это data class с изменяемыми полями (var), LaunchedEffect может не распознать изменение, так как Compose использует equals() для сравнения, а он для var-полей может работать непредсказуемо. Всегда используйте immutable объекты (val) или примитивы в качестве ключей LaunchedEffect.
Часто задаваемые вопросы
Если не передать ключи, LaunchedEffect не скомпилируется — Kotlin требует хотя бы один аргумент для vararg parameters. Используйте LaunchedEffect(Unit) для одноразового выполнения при входе в композицию или передавайте конкретные значения, при изменении которых эффект должен перезапускаться.
Нет, LaunchedEffect автоматически отменяет корутину при выходе composable из композиции, что предотвращает утечки памяти. Однако если корутина внутри LaunchedEffect удерживает ссылку на Activity или Context через замыкание, утечка возможна — используйте viewModelScope для долгоживущих операций в ViewModel.
LaunchedEffect выполняет корутину автоматически при входе в композицию с привязкой к ключам. rememberCoroutineScope предоставляет scope для ручного запуска корутин, например, в ответ на onItemClick. Используйте LaunchedEffect для автоматических side effects и rememberCoroutineScope для запуска корутин по событиям пользователя.
Если ключ LaunchedEffect — это unstable тип (например, var или класс без equals()), Compose может не распознать, что значение не изменилось, и будет перезапускать эффект при каждой рекомпозиции. Решение: используйте stable типы (примитивы, строки, data class с val полями) или оборачивайте mutable значения в remember.
Прямого способа остановить LaunchedEffect извне нет — управление идёт через ключи. Измените ключ, чтобы отменить текущую корутину. Если нужен полный контроль над жизненным циклом корутины, используйте rememberCoroutineScope с Job и вручную вызывайте job.cancel() по событию или при изменении состояния.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также