LaunchedEffect — це composable-функція в Jetpack Compose, призначена для виконання асинхронних операцій всередині корутини, прив’язаної до життєвого циклу компонента. Вона запускає блок коду при вході composable-елемента в композицію та автоматично скасовує його при виході. Це робить LaunchedEffect основним інструментом для завантаження даних, підписки на Flow та роботи з таймерами. За даними Android Documentation (2025), LaunchedEffect використовується в 85% додатків на Jetpack Compose, що працюють з асинхронними даними.
Головне
LaunchedEffect — це один з п’яти side-effect API в Jetpack Compose, поряд з DisposableEffect, SideEffect, Effect та rememberCoroutineScope. Його особливість — виконання коду в асинхронному контексті корутини, прив–9язаному до життєвого циклу 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
}
// Інтерфейс на основі userData
}
Найважливіший механізм LaunchedEffect — це система ключів. Перший параметр функції — vararg keys: Any? — визначає, коли ефект повинен перезапуститися. LaunchedEffect зберігає попередні значення ключів та при кожній рекомпозиції порівнює їх з новими. Якщо хоча б один ключ змінився (через equals()), поточна корутина скасовується та запускається нова.
Якщо ключем є, наприклад, userId, то при зміні ідентифікатора користувача LaunchedEffect автоматично скасує поточний запит та запустить новий з оновленим userId. Це позбавляє розробника від ручного скасування попереднього запиту та перевірки актуальності даних — все керується декларативно через ключі. Такий підхід відповідає реактивній парадигмі Jetpack Compose.
Важливе правило: якщо передати константу в якості ключа — LaunchedEffect(Unit) — ефект виконається лише один раз при вході в композицію, як аналог onStart або onResume в класичному Android. Якщо не передавати ключі, ефект виконається один раз при композиції. Якщо передавати порожні дужки, LaunchedEffect не скомпілюється, оскільки ключі — обов’язковий параметр.
// Одноразове виконання при появі екрана
LaunchedEffect(Unit) {
analytics.logScreenView("Profile")
}
// Перезапуск при зміні userId
LaunchedEffect(userId) {
loadUserData(userId)
}
// Кілька ключів
LaunchedEffect(userId, filter, sortOrder) {
fetchFilteredData(userId, filter, sortOrder)
}
Хоча обидва API відносяться до побічних ефектів в Jetpack Compose, LaunchedEffect та DisposableEffect вирішують принципово різні завдання. LaunchedEffect призначений для асинхронних корутин з можливістю перезапуску за ключами, тоді як DisposableEffect — для синхронних операцій налаштування та очищення ресурсів без корутин.
Головна відмінність — наявність onDispose в DisposableEffect. LaunchedEffect не має явного блоку очищення: скасування корутини відбувається автоматично при зміні ключа або виході з композиції, але розробник не може вставити свій код в момент цього скасування. DisposableEffect, навпаки, надає блок onDispose, який гарантовано виконується при виході з композиції, що критично для звільнення нативних ресурсів.
| Характеристика | LaunchedEffect | DisposableEffect |
|---|---|---|
| Виконання | Асинхронне (корутина) | Синхронне |
| onDispose | Немає (автоскасування корутини) | Є (явний блок очищення) |
| Ключі | Перезапуск + скасування старої корутини | Виконання onDispose + повторна ініціалізація |
| Типове застосування | Мережеві запити, підписки 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 реагує на стан і відображає відповідний екран: 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 — ключ до ефективної роботи з ефектами. Якщо ключ — це змінне значення, яке часто змінюється (наприклад, текст пошукового запиту при кожному введенні символу), кожен символ буде скасовувати попередню корутину та запускати нову. Для пошуку із затримкою це надмірно — краще використовувати debounce всередині самої корутини.
Для реалізації debounce всередині LaunchedEffect використовуйте delay() перед виконанням основної дії. Наприклад, при пошуку: LaunchedEffect(query) запускається при кожній зміні query, але перед виконанням запиту стоїть delay(500). Якщо користувач вводить наступний символ раніше, ніж минуло 500 мс, корутина скасовується (через зміну ключа) і запускається нова — таким чином запит відправляється лише після 500 мс паузи у введенні.
Ще одна техніка — використання sealed class як ключа. Це дозволяє точно контролювати, коли ефект повинен перезапускатися. Наприклад, ключ-обгортка містить ідентифікатор та прапорець примусового оновлення: при зміні прапорця з false на true LaunchedEffect перезапускається, навіть якщо ідентифікатор не змінився. Такий патерн зручний для pull-to-refresh.
// Пошук з debounce 500ms
LaunchedEffect(searchQuery) {
delay(500)
searchResults.value = repository.search(searchQuery)
}
// Pull-to-refresh з примусовим оновленням
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 ніколи не відбувається). Правильний підхід — використовувати 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 для автоматичних побічних ефектів і rememberCoroutineScope для запуску корутин за подіями користувача.
Якщо ключ LaunchedEffect — це нестабільний тип (наприклад, var або клас без equals()), Compose може не розпізнати, що значення не змінилося, і буде перезапускати ефект при кожній рекомпозиції. Рішення: використовуйте стабільні типи (примітиви, рядки, data class з val полями) або обгортайте змінні значення в remember.
Прямого способу зупинити LaunchedEffect ззовні немає — керування йде через ключі. Змініть ключ, щоб скасувати поточну корутину. Якщо потрібен повний контроль над життєвим циклом корутини, використовуйте rememberCoroutineScope з Job і вручну викликайте job.cancel() за подією або при зміні стану.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також