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
}
// Кориснички интерфејс базиран на userData
}
Најважнији механизам LaunchedEffect — систем кључева. Први параметар функције — vararg keys: Any? — одређује када ефекат треба да се поново покрене. LaunchedEffect чува претходне вредности кључева и при свакој рекомпозицији их упоређује са новим. Ако се бар један кључ променио (путем equals()), тренутна корутина се отказује и покреће се нова.
Ако је кључ, на пример, userId, онда ће при промени идентификатора корисника LaunchedEffect аутоматски отказати тренутни захтев и покренути нови са ажурираним userId. То ослобађа програмера ручног отказивања претходног захтева и провере актуелности података — све се управља декларативно путем кључева. Такав приступ одговара реактивној парадигми Jetpack Compose.
Важно правило: ако се проследи константа као кључ — LaunchedEffect(Unit) — ефекат ће се извршити само једном при уласку у композицију, као аналог onStart или onResume у класичном Android-у. Ако се не проследе кључеви — LaunchedEffect(Unit) — ефекат ће радити једном. Ако се проследе празне заграде — LaunchedEffect се неће компилирати, јер су кључеви обавезни параметар.
// Једнократно извршавање када се екран појави
LaunchedEffect(Unit) {
analytics.logScreenView("Profile")
}
// Поновно покретање када се userId промени
LaunchedEffect(userId) {
loadUserData(userId)
}
// Више кључева
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 + поновна иницијализација |
| Типична примена | Мрежни захтеви, 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 ms, корутина се отказује (због промене кључа) и покреће се нова — на тај начин захтев се шаље тек након 500 ms паузе у уносу.
Још једна техника — коришћење 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 никада не дешава), и блок onDispose неће моћи нормално да се заврши. Правилан приступ — коришћење collectLatest, који отказује претходну колекцију при пријему нове вредности.
Трећа грешка — прослеђивање угњеждених објеката као кључа. Ако је кључ data class са променљивим пољима (var), LaunchedEffect можда неће препознати промену, јер Compose користи equals() за поређење, а он за var-поља може радити непредвидиво. Увек користите immutable објекте (val) или примитиве као кључеве LaunchedEffect.
Често постављана питања
Ако се не проследе кључеви, LaunchedEffect се неће компилирати — Kotlin захтева бар један аргумент за vararg параметре. Користите LaunchedEffect(Unit) за једнократно извршавање при уласку у композицију или проследите конкретне вредности при чијој промени ефекат треба да се поново покрене.
Не, LaunchedEffect аутоматски отказује корутину при изласку composable из композиције, што спречава цурење меморије. Међутим, ако корутина унутар LaunchedEffect држи референцу на Activity или Context кроз затварање, цурење је могуће — користите viewModelScope за дугоживеће операције у ViewModel.
LaunchedEffect извршава корутину аутоматски при уласку у композицију са везивањем за кључеве. rememberCoroutineScope пружа scope за ручно покретање корутина, на пример, као одговор на onItemClick. Користите LaunchedEffect за аутоматске side effects и rememberCoroutineScope за покретање корутина по догађајима корисника.
Ако је кључ LaunchedEffect нестабилан тип (на пример, var или класа без equals()), Compose можда неће препознати да се вредност није променила и поново ће покретати ефекат при свакој рекомпозицији. Решење: користите стабилне типове (примитиве, стрингове, data class са val пољима) или умотајте mutable вредности у remember.
Не постоји директан начин да се заустави LaunchedEffect споља — управљање иде путем кључева. Промените кључ да бисте отказали тренутну корутину. Ако је потребна потпуна контрола над животним циклусом корутине, користите rememberCoroutineScope са Job и ручно позивајте job.cancel() по догађају или при промени стања.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође