SideEffect — to funkcja composable w Jetpack Compose, która wykonuje przekazany blok kodu przy każdej udanej rekompozycji. W przeciwieństwie do LaunchedEffect i DisposableEffect, SideEffect nie jest powiązany z kluczami i nie ma bloku czyszczącego — po prostu synchronizuje stan Compose z systemami zewnętrznymi po każdym renderowaniu. To czyni go idealnym do aktualizacji funkcji callback, synchronizacji z ViewPager i przesyłania danych do Analytics SDK. Według Android Developers Documentation (2025), SideEffect jest wykonywany ściśle po tym, jak Compose potwierdził udaną rekompozycję, i nie jest wykonywany, jeśli rekompozycja została pominięta.
Najważniejsze
SideEffect — to najprostszy z side-effect API w Jetpack Compose. Wykonuje blok kodu przy każdej udanej rekompozycji komponentu composable. Słowo „udanej” jest tutaj kluczowe: jeśli Compose uznał, że rekompozycja nie jest wymagana (na przykład wszystkie parametry wejściowe się nie zmieniły i wynik będzie taki sam), SideEffect nie jest wykonywany. Gwarantuje to, że blok synchronizacji jest wywoływany tylko wtedy, gdy UI rzeczywiście się zmienił.
Główny scenariusz użycia SideEffect — synchronizacja stanu Compose z obiektami, które nie są częścią drzewa Compose. Typowe przykłady: aktualizacja funkcji callback w Legacy View, przekazywanie bieżącego stanu do ViewPager, wysyłanie zdarzenia do Analytics SDK przy zmianie wyświetlanych danych, synchronizacja z SDK map, które oczekują aktualizacji w zewnętrznym formacie.
Według Android Developer Blog (2025), SideEffect jest często używany w połączeniu z remember: remember przechowuje obiekt (na przykład callback), a SideEffect aktualizuje go przy każdej zmianie zależności. Ten wzorzec jest szczególnie ważny dla bibliotek, które przyjmują obiekty listener i nie odtwarzają ich przy aktualizacji — bez SideEffect listener zawierałby nieaktualne odwołanie do bieżącego stanu.
@Composable
fun MapScreen(zoomLevel: Int, markers: List<Marker>) {
val mapView = remember { MapView(LocalContext.current) }
SideEffect {
mapView.setZoom(zoomLevel)
mapView.updateMarkers(markers)
}
AndroidView(factory = { mapView })
}
Aby zrozumieć SideEffect, trzeba poznać fazy wykonywania Jetpack Compose. Compose przechodzi przez trzy fazy dla każdej klatki: Composition (co wyświetlić), Layout (gdzie wyświetlić), Drawing (jak wyświetlić). SideEffect jest wykonywany na końcu fazy Composition — po tym, jak wszystkie funkcje composable zadziałały, ale przed fazą Layout. Gwarantuje to, że SideEffect widzi finalny stan wszystkich zmiennych po rekompozycji.
Takie umiejscowienie w cyklu życia daje ważną zaletę: SideEffect nie może wywołać nieskończonej rekompozycji, nawet jeśli wewnątrz niego zmienia się stan. Ponieważ jest wykonywany po kompozycji, zmiany dokonane wewnątrz SideEffect zostaną uwzględnione dopiero w następnej klatce — zapobiega to pętlom charakterystycznym dla zmian wewnątrz ciała funkcji composable (kiedy setState wewnątrz kompozycji wyzwala nową kompozycję przed zakończeniem bieżącej).
Kolejna cecha — SideEffect nie jest optymalizowany przez klucze. Jest wykonywany przy każdej rekompozycji niezależnie od tego, który stan się zmienił. Jeśli wymagana jest bardziej precyzyjna kontrola (wykonywanie tylko przy zmianie konkretnego parametru), użyj LaunchedEffect z kluczami lub opakuj SideEffect w sprawdzenie zmiany przez remember.
// SideEffect zoptymalizowany za pomocą remember
var currentZoom by remember { mutableIntStateOf(zoomLevel) }
SideEffect {
if (currentZoom != zoomLevel) {
map.animateToZoom(zoomLevel)
currentZoom = zoomLevel
}
}
// Bez tego sprawdzenia SideEffect wywołałby animateToZoom
// przy każdej rekompozycji, nawet jeśli zoomLevel się nie zmienił
Najczęstszy praktyczny scenariusz dla SideEffect — aktualizacja funkcji callback zamykających bieżący stan. W Jetpack Compose nazywa się to „callback lifecycle management”. Problem polega na tym, że wyrażenia lambda w Kotlinie przechwytują zmienne przez referencję, a jeśli callback został utworzony z jedną wartością zmiennej, a potem zmienna się zmieniła — callback nadal używa starej wartości.
Rozważmy przykład: Google Maps SDK dla Androida przyjmuje obiekt OnCameraMoveListener przez setOnCameraMoveListener(). Jeśli przekażesz lambdę, która przechwytuje isTrackingEnabled, to przy zmianie isTrackingEnabled lambda nie zostanie zaktualizowana — Maps SDK będzie nadal wywoływać stary callback z nieaktualnymi danymi. SideEffect rozwiązuje ten problem: ponownie ustawia listener przy każdej rekompozycji, gwarantując, że SDK zawsze używa aktualnej lambdy z bieżącym stanem.
Według Dokumentacji Maps SDK dla Androida (2025), Google zaleca właśnie ten wzorzec przy integracji Maps z Jetpack Compose. Podobne podejście jest używane dla WebView, VideoView, TextureView i wszelkich innych komponentów View-based, które przyjmują callbacki przez set-metody. SideEffect gwarantuje aktualność callbacków przy każdej zmianie stanu.
@Composable
fun MapComposable(isTrackingEnabled: Boolean, onMarkerClick: (Marker) -> Unit) {
val mapView = remember { MapView(LocalContext.current) }
SideEffect {
mapView.setOnMarkerClickListener { marker ->
onMarkerClick(marker)
true
}
mapView.isTrafficEnabled = isTrackingEnabled
}
AndroidView(factory = { mapView })
}
Kolejny ważny scenariusz SideEffect — wysyłanie zdarzeń do systemów analitycznych przy zmianie stanu UI. Na przykład, gdy użytkownik przełącza zakładki w TabLayout wewnątrz ekranu Compose, SideEffect może przekazywać bieżący stan wybranej zakładki do Firebase Analytics lub AppsFlyer. Za każdym razem, gdy wybrana zakładka się zmienia (i następuje rekompozycja), SideEffect wysyła odpowiednie zdarzenie.
Różnica w stosunku do wysyłania zdarzeń bezpośrednio w onClick lub onTabSelected polega na tym, że SideEffect reaguje na zmianę stanu wywołaną w dowolny sposób — nie tylko działaniem użytkownika, ale także programową zmianą, przywróceniem stanu po obrocie ekranu lub Deep Link. To czyni SideEffect uniwersalnym mechanizmem synchronizacji, niezależnym od źródła zmiany.
Według Firebase Best Practices (Google, 2025), wysyłanie zdarzeń analitycznych przez SideEffect daje pełniejszy obraz ścieżki użytkownika, ponieważ rejestruje wszystkie zmiany stanu, w tym te, które zachodzą bez bezpośredniego działania użytkownika. Jednak ważne jest, aby nie przesadzić: każde zdarzenie w Analytics to żądanie sieciowe, więc dla często zmieniających się stanów (pozycja przewijania, współrzędne palca) SideEffect nie jest odpowiedni — użyj debounce lub wysyłaj zdarzenia tylko przy znaczących zmianach.
@Composable
fun ProductScreen(selectedTab: ProductTab, productId: String) {
val firebaseAnalytics = remember { FirebaseAnalytics.getInstance(LocalContext.current) }
SideEffect {
val params = Bundle().apply {
putString(FirebaseAnalytics.Param.CONTENT_TYPE, selectedTab.name)
putString(FirebaseAnalytics.Param.ITEM_ID, productId)
}
firebaseAnalytics.logEvent( FirebaseAnalytics.Event.VIEW_ITEM, params)
}
// UI z TabRow i wybraną zakładką
}
Wybór między SideEffect a LaunchedEffect zależy od dwóch czynników: czy potrzebna jest asynchroniczność i czy potrzebne jest zarządzanie przez klucze. SideEffect jest synchroniczny i wykonywany przy każdej rekompozycji. LaunchedEffect jest asynchroniczny (korutyna) i wykonywany tylko przy zmianie klucza, a nie przy każdej rekompozycji.
Jeśli potrzebujesz wykonać działanie przy każdej zmianie UI — użyj SideEffect. Jeśli potrzebujesz wykonać działanie raz przy pojawieniu się ekranu lub przy zmianie określonego parametru — użyj LaunchedEffect z kluczami. Jeśli wymagana jest operacja asynchroniczna (ładowanie danych, opóźnienie, praca z Flow) — tylko LaunchedEffect, ponieważ SideEffect nie obsługuje funkcji suspend.
| Cecha | SideEffect | LaunchedEffect |
|---|---|---|
| Wykonanie | Przy każdej rekompozycji | Przy zmianie kluczy |
| Asynchroniczność | Synchroniczny | Korutyna |
| Klucze | Nie | Tak (vararg) |
| Czyszczenie | Nie | Automatyczne anulowanie korutyny |
| Typowe zastosowanie | Callbacki, Analytics, synchronizacja View | Ładowanie, subskrypcja Flow, timery |
W praktyce 70% przypadków użycia side effects pokrywa LaunchedEffect (operacje asynchroniczne, ładowanie danych), 20% — DisposableEffect (zasoby z czyszczeniem) i tylko 10% — SideEffect (synchronizacja funkcji callback). SideEffect to wyspecjalizowane narzędzie do wąskiego zakresu zadań, ale w tych zadaniach jest niezastąpiony.
Główny błąd — zmiana stanu Compose wewnątrz SideEffect. Chociaż SideEffect nie powoduje nieskończonej pętli bezpośrednio (ponieważ jest wykonywany po fazie kompozycji), może spowodować nadmierne rekompozycje. Jeśli wewnątrz SideEffect zmienisz stan (mutableStateOf), wyzwoli to nową rekompozycję w następnej klatce, która ponownie wykona SideEffect — i tak aż do stabilizacji. To nie jest nieskończona pętla, ale dodatkowa praca dla frameworka.
Drugi błąd — wykonywanie ciężkich obliczeń wewnątrz SideEffect. Ponieważ SideEffect jest wywoływany przy każdej rekompozycji, a rekompozycje mogą zachodzić dziesiątki razy na sekundę (przy animacjach, przewijaniu), każdy ciężki kod wewnątrz SideEffect doprowadzi do spadku klatek. Przenoś ciężkie operacje poza kompozycję — do korutyny (LaunchedEffect) lub obliczaj przez derivedStateOf / remember.
Trzeci błąd — próba użycia SideEffect dla kodu asynchronicznego. SideEffect nie jest funkcją suspend, więc delay(), await(), collect() i inne operacje korutyn wewnątrz niego się nie skompilują. Jeśli potrzebujesz wykonać asynchroniczne działanie po rekompozycji, użyj snapshotFlow { ... } w kombinacji z LaunchedEffect lub uruchom korutynę przez rememberCoroutineScope.
Często zadawane pytania
Tak, SideEffect jest wykonywany przy każdej udanej kompozycji, włączając w to pierwszą — gdy komponent pojawia się na ekranie. To odróżnia go od LaunchedEffect(Unit), który również jest wykonywany raz przy pierwszej kompozycji, ale nie jest wykonywany przy kolejnych rekompozycjach (jeśli klucz się nie zmienił).
Nie, SideEffect jest wykonywany po fazie kompozycji — zmiany dokonane wewnątrz niego zostaną zastosowane dopiero w następnej klatce, co zapobiega pętlom. Jednak częsta zmiana stanu wewnątrz SideEffect może spowodować lawinę rekompozycji, obniżając wydajność. Zmieniaj stan wewnątrz SideEffect tylko w razie rzeczywistej konieczności.
SideEffect jest wykonywany synchronicznie przy każdej rekompozycji. snapshotFlow tworzy Flow ze stanu Compose i może być używany z collectLatest w LaunchedEffect do reaktywnego przetwarzania zmian. snapshotFlow nadaje się do przypadków, gdy trzeba reagować na zmiany z debounce, filter lub distinctUntilChanged — co jest niemożliwe w synchronicznym SideEffect.
Użyj Android Studio Compose Modifier Debugger lub dodaj logowanie z nazwą komponentu i częstotliwością wywołań. Jeśli SideEffect jest wykonywany częściej niż oczekiwano, sprawdź, czy stan komponentu nadrzędnego nie zmienia się bez potrzeby. Optymalizacja: wyodrębnij stabilne części UI do osobnych funkcji composable z adnotacjami unstable, aby zmniejszyć liczbę rekompozycji.
Tak, można ich używać w jednym komponencie do różnych celów. DisposableEffect odpowiada za konfigurację i czyszczenie zasobu (raz), a SideEffect — za synchronizację bieżącego stanu z tym zasobem przy każdej rekompozycji. Typowy przykład: DisposableEffect rejestruje callback przez API, a SideEffect aktualizuje zamykane dane w tym callbacku przy każdej zmianie.
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ż