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 за стартиране на корутини в контекста на композицията.
  • Ключове — при промяна на ключовете корутината се отменя и рестартира с нови стойности.
  • Автоматично отменяне — корутината автоматично се отменя при излизане на component от композицията.
  • Асинхронност — блокът се изпълнява в CoroutineScope с диспечер Dispatchers.Main.
  • Зареждане на данни — типичен сценарий: зареждане от мрежата при първото появяване на екрана.

Какво е LaunchedEffect в 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, което осигурява по-предвидимо управление на жизнения цикъл и елиминира изтичания на памет, характерни за абонаменти без изрично отменяне.

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(Unit) — ефектът ще работи веднъж. Ако се предадат празни скоби — LaunchedEffect няма да се компилира, тъй като ключовете са задължителен параметър.

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

// Рестартиране при промяна на userId
LaunchedEffect(userId) {
    loadUserData(userId)
}

// Няколко ключа
LaunchedEffect(userId, filter, sortOrder) {
    fetchFilteredData(userId, filter, sortOrder)
}

Разлика между LaunchedEffect и DisposableEffect

Въпреки че и двата API принадлежат към side effects в 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, резултатът се присвоява на променлива за състояние и 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 ms, корутината се отменя (поради промяна на ключа) и се стартира нова — по този начин заявката се изпраща само след 500 ms пауза във въвеждането.

Друга техника — използване на 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 никога не се случва) и блокът onDispose няма да може да завърши нормално. Правилният подход — използване на collectLatest, който отменя предишната колекция при получаване на нова стойност.

Трета грешка — предаване на вложени обекти като ключ. Ако ключът е data class с променливи полета (var), LaunchedEffect може да не разпознае промяната, тъй като Compose използва equals() за сравнение, а той за var-полета може да работи непредвидимо. Винаги използвайте неизменяеми обекти (val) или примитиви като ключове на LaunchedEffect.

Често задавани въпроси

Какво ще стане, ако не се предаде ключ на LaunchedEffect?

Ако не се предадат ключове, LaunchedEffect няма да се компилира — Kotlin изисква поне един аргумент за vararg параметри. Използвайте LaunchedEffect(Unit) за еднократно изпълнение при влизане в композицията или предайте конкретни стойности, при промяна на които ефектът трябва да се рестартира.

Може ли LaunchedEffect да причини изтичане на памет?

Не, LaunchedEffect автоматично отменя корутината при излизане на composable от композицията, което предотвратява изтичане на памет. Въпреки това, ако корутината вътре в LaunchedEffect държи референция към Activity или Context чрез затваряне, изтичане е възможно — използвайте viewModelScope за дългоживеещи операции в ViewModel.

Каква е разликата между LaunchedEffect и rememberCoroutineScope?

LaunchedEffect изпълнява корутината автоматично при влизане в композицията с обвързване към ключове. rememberCoroutineScope предоставя scope за ръчно стартиране на корутини, например в отговор на onItemClick. Използвайте LaunchedEffect за автоматични side effects и rememberCoroutineScope за стартиране на корутини по събития на потребителя.

Защо LaunchedEffect се изпълнява няколко пъти при рекомпозиция?

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

Как ръчно да спра LaunchedEffect?

Няма директен начин за спиране на LaunchedEffect отвън — управлението става чрез ключове. Променете ключа, за да отмените текущата корутина. Ако имате нужда от пълен контрол над жизнения цикъл на корутината, използвайте rememberCoroutineScope с Job и ръчно извиквайте job.cancel() при събитие или промяна на състоянието.

Резюме

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

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също