LaunchedEffect — to funkcja composable w Jetpack Compose, przeznaczona do wykonywania operacji asynchronicznych wewnątrz korutyny, powiązanej z cyklem życia komponentu. Uruchamia blok kodu przy wejściu elementu composable do kompozycji i automatycznie go anuluje przy wyjściu. To czyni LaunchedEffect głównym narzędziem do ładowania danych, subskrypcji Flow i pracy z timerami. Według Android Documentation (2025), LaunchedEffect jest używany w 85% aplikacji na Jetpack Compose pracujących z danymi asynchronicznymi.
Najważniejsze
LaunchedEffect — to jeden z pięciu side-effect API w Jetpack Compose, obok DisposableEffect, SideEffect, SnapshotEffect i rememberCoroutineScope. Jego cechą szczególną jest wykonywanie kodu w asynchronicznym kontekście korutyny, powiązanym z cyklem życia elementu composable. W przeciwieństwie do zwykłych funkcji callback, LaunchedEffect nie blokuje UI i może wykonywać długotrwałe operacje, takie jak zapytania sieciowe lub oczekiwanie na opóźnienie.
Pod maską LaunchedEffect używa CoroutineScope, który jest dostarczany przez kompozycję. Ten scope jest automatycznie anulowany, gdy element composable opuszcza kompozycję. Takie powiązanie gwarantuje, że żadna korutyna nie będzie kontynuować działania po zamknięciu ekranu — to kluczowa różnica w porównaniu z globalnymi korutynami w ViewModel lub Application scope.
Według Android Developers Blog (2025), LaunchedEffect został specjalnie zaprojektowany do zastąpienia wzorca LiveData-observer w świecie Compose. Zamiast subskrybować LiveData przez observeAsState i osobno zarządzać subskrypcją, programiści używają LaunchedEffect z collectAsState na Flow, co daje bardziej przewidywalne zarządzanie cyklem życia i eliminuje wycieki pamięci charakterystyczne dla subskrypcji bez jawnego anulowania.
@Composable
fun UserProfileScreen(userId: Int) {
var userData by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
val result = userRepository.fetchUser(userId)
userData = result
}
// Interfejs na podstawie userData
}
Najważniejszy mechanizm LaunchedEffect — to system kluczy. Pierwszy parametr funkcji — vararg keys: Any? — określa, kiedy efekt powinien zostać ponownie uruchomiony. LaunchedEffect zapisuje poprzednie wartości kluczy i przy każdej rekompozycji porównuje je z nowymi. Jeśli choć jeden klucz się zmienił (przez equals()), bieżąca korutyna jest anulowana i uruchamiana jest nowa.
Jeśli kluczem jest na przykład userId, to przy zmianie identyfikatora użytkownika LaunchedEffect automatycznie anuluje bieżące zapytanie i uruchomi nowe z zaktualizowanym userId. To zwalnia programistę z ręcznego anulowania poprzedniego zapytania i sprawdzania aktualności danych — wszystko jest zarządzane deklaratywnie przez klucze. Takie podejście odpowiada reaktywnej paradigmie Jetpack Compose.
Ważna zasada: jeśli przekazać stałą jako klucz — LaunchedEffect(Unit) — efekt wykona się tylko raz przy wejściu do kompozycji, jako analog onStart lub onResume w klasycznym Androidzie. Jeśli nie przekazywać kluczy — LaunchedEffect(Unit) — efekt zadziała raz. Jeśli przekazać puste nawiasy — LaunchedEffect nie skompiluje się, ponieważ klucze są obowiązkowym parametrem.
// Jednorazowe wykonanie po pojawieniu się ekranu
LaunchedEffect(Unit) {
analytics.logScreenView("Profile")
}
// Restart przy zmianie userId
LaunchedEffect(userId) {
loadUserData(userId)
}
// Wiele kluczy
LaunchedEffect(userId, filter, sortOrder) {
fetchFilteredData(userId, filter, sortOrder)
}
Chociaż oba API należą do side effects w Jetpack Compose, LaunchedEffect i DisposableEffect rozwiązują zasadniczo różne zadania. LaunchedEffect jest przeznaczony do asynchronicznych korutyn z możliwością ponownego uruchamiania według kluczy, podczas gdy DisposableEffect — do synchronicznych operacji konfiguracji i zwalniania zasobów bez korutyn.
Główna różnica — obecność onDispose w DisposableEffect. LaunchedEffect nie ma jawnego bloku oczyszczania: anulowanie korutyny następuje automatycznie przy zmianie klucza lub wyjściu z kompozycji, ale programista nie może wstawić własnego kodu w momencie tego anulowania. DisposableEffect natomiast dostarcza blok onDispose, który jest gwarantowanie wykonywany przy wyjściu z kompozycji, co jest krytyczne dla zwalniania natywnych zasobów.
| Charakterystyka | LaunchedEffect | DisposableEffect |
|---|---|---|
| Wykonanie | Asynchroniczne (korutyna) | Synchroniczne |
| onDispose | Nie (autoanulowanie korutyny) | Tak (jawny blok oczyszczania) |
| Klucze | Ponowne uruchomienie + anulowanie starej korutyny | Wykonanie onDispose + ponowna inicjalizacja |
| Typowe zastosowanie | Zapytania sieciowe, subskrypcje Flow, timery | BroadcastReceiver, czujniki, natywne listener |
| Anulowanie przy wyjściu | Automatyczne | Przez onDispose |
Według Artykułu Google „Compose Side Effects: Deep Dive” (2025), prawidłowy wybór między LaunchedEffect a DisposableEffect określa typ zasobu: jeśli operacja to korutyna z możliwością anulowania — użyj LaunchedEffect. Jeśli zasób wymaga jawnego wywołania close(), unregister() lub dispose() — użyj DisposableEffect.
Najczęstszy scenariusz użycia LaunchedEffect — ładowanie danych przy otwarciu ekranu. Wzór jest prosty: wewnątrz LaunchedEffect wywoływana jest suspend-funkcja repozytorium lub UseCase, wynik przypisywany jest do zmiennej stanu, a UI automatycznie się przerysowuje. LaunchedEffect gwarantuje, że przy ponownym otwarciu ekranu (na przykład po powrocie) ładowanie jest wykonywane ponownie, jeśli klucze się zmieniły.
Do wyświetlania stanów ładowania używana jest trójka stanów: Loading, Success, Error. LaunchedEffect jest owinięty w try-catch, a przy sukcesie ustawiany jest state = Success(data), przy błędzie — state = Error(exception). UI reaguje na state i wyświetla odpowiedni ekran: shimmer-ładowarkę, dane lub ekran błędu z przyciskiem ponawiania.
Jeśli wymagane jest ładowanie danych przy przewijaniu (pagination), LaunchedEffect jest łączony z LazyColumn i LazyListState: po osiągnięciu końca listy klucz LaunchedEffect jest aktualizowany (na przykład licznik stron), co wyzwala ładowanie następnej porcji danych.
@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) }
}
}
Prawidłowe używanie kluczy LaunchedEffect — klucz do efektywnej pracy z efektami (tu gra słów oddaje sedno). Jeśli klucz to mutable wartość, która często się zmienia (na przykład tekst zapytania wyszukiwania przy każdym wprowadzaniu znaku), każdy znak będzie anulować poprzednią korutynę i uruchamiać nową. Dla wyszukiwania z opóźnieniem jest to zbędne — lepiej użyć debounce wewnątrz samej korutyny.
Do implementacji debounce wewnątrz LaunchedEffect użyj delay() przed wykonaniem głównego działania. Na przykład przy wyszukiwaniu: LaunchedEffect(query) uruchamia się przy każdej zmianie query, ale przed wykonaniem zapytania następuje delay(500). Jeśli użytkownik wprowadzi następny znak wcześniej niż po 500 ms, korutyna jest anulowana (z powodu zmiany klucza) i uruchamiana jest nowa — w ten sposób zapytanie jest wysyłane dopiero po 500 ms pauzy w wprowadzaniu.
Inna technika — użycie sealed class jako klucza. Pozwala to precyzyjnie kontrolować, kiedy efekt powinien zostać ponownie uruchomiony. Na przykład klucz-opakowanie zawiera identyfikator i flagę wymuszonej aktualizacji: przy zmianie flagi z false na true LaunchedEffect uruchamia się ponownie, nawet jeśli identyfikator się nie zmienił. Taki wzór jest wygodny dla pull-to-refresh.
// Wyszukiwanie z debounce 500ms
LaunchedEffect(searchQuery) {
delay(500)
searchResults.value = repository.search(searchQuery)
}
// Pull-to-refresh z wymuszoną aktualizacją
data class RefreshKey(val id: Int, val refreshTrigger: Int)
var refreshTrigger by remember { mutableIntStateOf(0) }
LaunchedEffect(RefreshKey(userId, refreshTrigger)) {
articles = repository.loadUserArticles(userId)
}
Pierwszy i najczęstszy błąd — użycie LaunchedEffect bez kluczy. Jeśli napisać LaunchedEffect { ... } bez argumentów, korutyna będzie uruchamiana ponownie przy każdej rekompozycji, co doprowadzi do nieskończonej pętli zapytań. LaunchedEffect wymaga co najmniej jednego klucza — zwykle Unit dla jednorazowego wykonania.
Drugi błąd — próba użycia LaunchedEffect do subskrypcji Flow bez collect. Jeśli wewnątrz LaunchedEffect wywołać collect na Flow, korutyna zostanie wstrzymana do zakończenia Flow (które w przypadku StateFlow nigdy nie następuje), a blok onDispose nie będzie mógł normalnie się zakończyć. Prawidłowym podejściem jest użycie collectLatest, które anuluje poprzednią kolekcję po otrzymaniu nowej wartości.
Trzeci błąd — przekazywanie zagnieżdżonych obiektów jako klucza. Jeśli klucz to data class z mutowalnymi polami (var), LaunchedEffect może nie rozpoznać zmiany, ponieważ Compose używa equals() do porównania, a on dla var-pól może działać nieprzewidywalnie. Zawsze używaj immutable obiektów (val) lub prymitywów jako kluczy LaunchedEffect.
Często zadawane pytania
Jeśli nie przekazać kluczy, LaunchedEffect nie skompiluje się — Kotlin wymaga co najmniej jednego argumentu dla vararg parameters. Użyj LaunchedEffect(Unit) do jednorazowego wykonania przy wejściu do kompozycji lub przekazuj konkretne wartości, przy zmianie których efekt powinien być ponownie uruchamiany.
Nie, LaunchedEffect automatycznie anuluje korutynę przy wyjściu composable z kompozycji, co zapobiega wyciekom pamięci. Jednak jeśli korutyna wewnątrz LaunchedEffect przechowuje referencję do Activity lub Context przez domknięcie, wyciek jest możliwy — użyj viewModelScope dla długożywotnych operacji w ViewModel.
LaunchedEffect wykonuje korutynę automatycznie przy wejściu do kompozycji z powiązaniem z kluczami. rememberCoroutineScope dostarcza scope do ręcznego uruchamiania korutyn, na przykład w odpowiedzi na onItemClick. Użyj LaunchedEffect do automatycznych side effects i rememberCoroutineScope do uruchamiania korutyn na zdarzenia użytkownika.
Jeśli klucz LaunchedEffect to niestabilny typ (na przykład var lub klasa bez equals()), Compose może nie rozpoznać, że wartość się nie zmieniła, i będzie ponownie uruchamiać efekt przy każdej rekompozycji. Rozwiązanie: używaj stabilnych typów (prymitywy, stringi, data class z val polami) lub zawiń mutable wartości w remember.
Nie ma bezpośredniego sposobu na zatrzymanie LaunchedEffect z zewnątrz — sterowanie odbywa się przez klucze. Zmień klucz, aby anulować bieżącą korutynę. Jeśli potrzebujesz pełnej kontroli nad cyklem życia korutyny, użyj rememberCoroutineScope z Job i ręcznie wywołuj job.cancel() na zdarzenie lub przy zmianie stanu.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również