LaunchedEffect: що це, корутини та керування в Jetpack Compose

Автор: IT Sectr Опубліковано: 2026-06-30 Час читання: 9 хв

LaunchedEffect — це composable-функція в Jetpack Compose, призначена для виконання асинхронних операцій всередині корутини, прив’язаної до життєвого циклу компонента. Вона запускає блок коду при вході composable-елемента в композицію та автоматично скасовує його при виході. Це робить LaunchedEffect основним інструментом для завантаження даних, підписки на Flow та роботи з таймерами. За даними Android Documentation (2025), LaunchedEffect використовується в 85% додатків на Jetpack Compose, що працюють з асинхронними даними.

Головне

  • LaunchedEffect — side-effect API для запуску корутин в контексті композиції.
  • Ключі — при зміні ключів корутина скасовується та перезапускається з новими значеннями.
  • Автоскасування — корутина автоматично скасовується при виході компонента з композиції.
  • Асинхронність — блок виконується в CoroutineScope з диспетчером Dispatchers.Main.
  • Завантаження даних — типовий сценарій: завантаження з мережі при першій появі екрана.

Що таке LaunchedEffect в 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, що дає більш передбачуване керування життєвим циклом та усуває витоки пам’яті, властиві підпискам без явного скасування.

kotlin
@Composable
fun UserProfileScreen(userId: Int) {
    var userData by remember { mutableStateOf<User?>(null) }
    
    LaunchedEffect(userId) {
        val result = userRepository.fetchUser(userId)
        userData = result
    }
    
    // Інтерфейс на основі userData
}

Як працює LaunchedEffect з ключами

Найважливіший механізм LaunchedEffect — це система ключів. Перший параметр функції — vararg keys: Any? — визначає, коли ефект повинен перезапуститися. LaunchedEffect зберігає попередні значення ключів та при кожній рекомпозиції порівнює їх з новими. Якщо хоча б один ключ змінився (через equals()), поточна корутина скасовується та запускається нова.

Якщо ключем є, наприклад, userId, то при зміні ідентифікатора користувача LaunchedEffect автоматично скасує поточний запит та запустить новий з оновленим userId. Це позбавляє розробника від ручного скасування попереднього запиту та перевірки актуальності даних — все керується декларативно через ключі. Такий підхід відповідає реактивній парадигмі Jetpack Compose.

Важливе правило: якщо передати константу в якості ключа — LaunchedEffect(Unit) — ефект виконається лише один раз при вході в композицію, як аналог onStart або onResume в класичному Android. Якщо не передавати ключі, ефект виконається один раз при композиції. Якщо передавати порожні дужки, LaunchedEffect не скомпілюється, оскільки ключі — обов’язковий параметр.

kotlin
// Одноразове виконання при появі екрана
LaunchedEffect(Unit) {
    analytics.logScreenView("Profile")
}

// Перезапуск при зміні userId
LaunchedEffect(userId) {
    loadUserData(userId)
}

// Кілька ключів
LaunchedEffect(userId, filter, sortOrder) {
    fetchFilteredData(userId, filter, sortOrder)
}

Відмінність LaunchedEffect від DisposableEffect

Хоча обидва API відносяться до побічних ефектів в Jetpack Compose, LaunchedEffect та DisposableEffect вирішують принципово різні завдання. LaunchedEffect призначений для асинхронних корутин з можливістю перезапуску за ключами, тоді як DisposableEffect — для синхронних операцій налаштування та очищення ресурсів без корутин.

Головна відмінність — наявність onDispose в DisposableEffect. LaunchedEffect не має явного блоку очищення: скасування корутини відбувається автоматично при зміні ключа або виході з композиції, але розробник не може вставити свій код в момент цього скасування. DisposableEffect, навпаки, надає блок onDispose, який гарантовано виконується при виході з композиції, що критично для звільнення нативних ресурсів.

ХарактеристикаLaunchedEffectDisposableEffect
ВиконанняАсинхронне (корутина)Синхронне
onDisposeНемає (автоскасування корутини)Є (явний блок очищення)
КлючіПерезапуск + скасування старої корутиниВиконання onDispose + повторна ініціалізація
Типове застосуванняМережеві запити, підписки Flow, таймериBroadcastReceiver, сенсори, нативні listener
Скасування при виходіАвтоматичнеЧерез onDispose

За даними Статті Google “Compose Side Effects: Deep Dive” (2025), правильний вибір між LaunchedEffect та DisposableEffect визначається типом ресурсу: якщо операція — корутина з можливістю скасування — використовуйте LaunchedEffect. Якщо ресурс вимагає явного виклику close(), unregister() або dispose() — використовуйте DisposableEffect.

Завантаження даних через LaunchedEffect

Найбільш частий сценарій використання 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 оновлюється (наприклад, лічильник сторінок), що тригерить завантаження наступної порції даних.

kotlin
@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.

kotlin
// Пошук з 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 { ... } без аргументів, корутина буде перезапускатися при кожній рекомпозиції, що призведе до нескінченного циклу запитів. LaunchedEffect вимагає хоча б один ключ — зазвичай Unit для одноразового виконання.

Друга помилка — спроба використовувати LaunchedEffect для підписки на Flow без collect. Якщо всередині LaunchedEffect викликати collect на Flow, корутина призупиниться до завершення Flow (яке у випадку StateFlow ніколи не відбувається). Правильний підхід — використовувати collectLatest, який скасовує попередню колекцію при надходженні нового значення.

Третя помилка — передача вкладених об’єктів в якості ключа. Якщо ключ — це data class зі змінними полями (var), LaunchedEffect може не розпізнати зміну, оскільки Compose використовує equals() для порівняння, а він для var-полів може працювати непередбачувано. Завжди використовуйте immutable об’єкти (val) або примітиви в якості ключів LaunchedEffect.

Часті запитання

Що буде, якщо не передати ключ в LaunchedEffect?

Якщо не передати ключі, LaunchedEffect не скомпілюється — Kotlin вимагає хоча б один аргумент для vararg parameters. Використовуйте LaunchedEffect(Unit) для одноразового виконання при вході в композицію або передавайте конкретні значення, при зміні яких ефект повинен перезапускатися.

Чи може LaunchedEffect викликати витік пам’яті?

Ні, LaunchedEffect автоматично скасовує корутину при виході composable з композиції, що запобігає витокам пам’яті. Однак якщо корутина всередині LaunchedEffect утримує посилання на Activity або Context через замикання, витік можливий — використовуйте viewModelScope для довгоживучих операцій в ViewModel.

У чому різниця між LaunchedEffect та rememberCoroutineScope?

LaunchedEffect виконує корутину автоматично при вході в композицію з прив’язкою до ключів. rememberCoroutineScope надає scope для ручного запуску корутин, наприклад, у відповідь на onItemClick. Використовуйте LaunchedEffect для автоматичних побічних ефектів і rememberCoroutineScope для запуску корутин за подіями користувача.

Чому LaunchedEffect виконується кілька разів при рекомпозиції?

Якщо ключ LaunchedEffect — це нестабільний тип (наприклад, var або клас без equals()), Compose може не розпізнати, що значення не змінилося, і буде перезапускати ефект при кожній рекомпозиції. Рішення: використовуйте стабільні типи (примітиви, рядки, data class з val полями) або обгортайте змінні значення в remember.

Як зупинити LaunchedEffect вручну?

Прямого способу зупинити LaunchedEffect ззовні немає — керування йде через ключі. Змініть ключ, щоб скасувати поточну корутину. Якщо потрібен повний контроль над життєвим циклом корутини, використовуйте rememberCoroutineScope з Job і вручну викликайте job.cancel() за подією або при зміні стану.

Підсумки

  • LaunchedEffect — side-effect API Jetpack Compose для запуску асинхронних корутин, прив’язаних до життєвого циклу composable.
  • Ключі — система перезапуску за ключами: зміна ключа скасовує поточну корутину та запускає нову з оновленими параметрами.
  • Автоскасування — корутина автоматично скасовується при виході composable з композиції, запобігаючи витокам пам’яті.
  • Завантаження даних — типовий патерн: LaunchedEffect(ключ) для завантаження даних з мережі з обробкою станів Loading, Success, Error.
  • Debounce — реалізується через delay() всередині LaunchedEffect: при зміні ключа до закінчення затримки корутина скасовується.
  • Підписка Flow — використовуйте collectLatest замість collect для коректної обробки Flow всередині LaunchedEffect.
  • Стабільні ключі — застосовуйте лише immutable типи (val, примітиви, data class) в якості ключів для передбачуваної поведінки.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також