DisposableEffect to funkcja composable w Jetpack Compose przeznaczona do operacji wymagających jawnej inicjalizacji i późniejszego czyszczenia zasobów. W przeciwieństwie do innych API side-effect, DisposableEffect udostępnia blok onDispose, który gwarantowanie wykonuje się przy opuszczeniu kompozycji przez komponent lub przy zmianie klucza. To czyni go niezastąpionym do pracy z natywnymi subskrypcjami, słuchaczami sensorów i zasobami sprzętowymi. Według Android Developers Documentation (2025), DisposableEffect jest zalecany we wszystkich scenariuszach wymagających pary setup/teardown, analogicznej do onStart/onStop w cyklu życia Activity.
Najważniejsze
DisposableEffect to kluczowe narzędzie do zarządzania zasobami w Jetpack Compose. Jego główna cecha to gwarantowane wywołanie bloku onDispose przy zakończeniu cyklu życia komponentu composable. To zachowanie jest krytyczne dla programowania na Androida, gdzie niezamknięte subskrypcje do usług systemowych mogą prowadzić do wycieków pamięci i awarii aplikacji.
W przeciwieństwie do LaunchedEffect, który działa w asynchronicznym kontekście korutyny, DisposableEffect wykonuje się synchronicznie. Oznacza to, że wewnątrz niego nie można wywoływać funkcji suspend. Synchroniczność zapewnia przewidywalność: możesz być pewien, że kod inicjalizacji wykona się przed pierwszym renderowaniem, a kod czyszczenia — zanim komponent zostanie usunięty z pamięci.
Według Dokumentacji Jetpack Compose (2025), DisposableEffect należy stosować w czterech głównych scenariuszach: (1) subskrypcja usług systemowych (sensory, LocationManager), (2) rejestracja BroadcastReceiver, (3) praca z bibliotekami opartymi na callback, które nie obsługują korutyn, (4) wiązanie komponentów Compose z systemami Legacy View przez AndroidView.
class SensorManager(private val context: Context) {
fun startListening(callback: (Float) -> Unit) { /* register */ }
fun stopListening() { /* cancel */ }
}
@Composable
fun SensorDisplay() {
val sensorManager = remember { SensorManager(context) }
var value by remember { mutableStateOf(0f) }
DisposableEffect(Unit) {
sensorManager.startListening { value = it }
onDispose { sensorManager.stopListening() }
}
Text("Czujnik: $value")
}
Wewnętrzna mechanika DisposableEffect opiera się na fazach cyklu życia kompozycji. Gdy komponent composable wchodzi do kompozycji, DisposableEffect wywołuje przekazany blok kodu. Blok ten zwraca obiekt DisposableEffectResult zawierający lambdę onDispose. Kompozycja przechowuje ten wynik i wywołuje onDispose w momencie, gdy komponent opuszcza kompozycję — niezależnie od przyczyny (nawigacja, zmiana stanu rodzica, usunięcie z LazyColumn).
Mechanizm kluczy w DisposableEffect działa analogicznie do LaunchedEffect: przy zmianie dowolnego klucza najpierw wykonuje się onDispose dla starego stanu, następnie blok inicjalizacji uruchamiany jest ponownie z nowymi kluczami. Pozwala to na rekonfigurację zasobu przy zmianie jego parametrów. Na przykład, jeśli kluczem jest URL socketu, przy jego zmianie stary socket jest zamykany i otwierany jest nowy.
Ważne: blok onDispose to obowiązkowy element DisposableEffect. Jeśli nie wywołasz onDispose wewnątrz bloku, kod się nie skompiluje. To wymaganie kompilatora gwarantuje, że programista nie zapomni przewidzieć czyszczenia zasobu, co jest częstą przyczyną błędów przy ręcznym zarządzaniu subskrypcjami.
// Prawidłowe użycie z kluczem
DisposableEffect(sensorType) {
val sensor = sensorManager.getDefaultSensor(sensorType)
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
onDispose {
sensorManager.unregisterListener(listener)
}
}
// Wiele zasobów w jednym DisposableEffect
DisposableEffect(Unit) {
context.registerReceiver(receiver, intentFilter)
lifecycle.addObserver(observer)
onDispose {
context.unregisterReceiver(receiver)
lifecycle.removeObserver(observer)
}
}
Wycieki pamięci w aplikacjach Android często powstają z powodu niezarejestrowanych słuchaczy i subskrypcji, które nadal przechowują referencję do Activity lub Context po zamknięciu ekranu. DisposableEffect rozwiązuje ten problem na poziomie frameworka: jeśli programista użył DisposableEffect do rejestracji słuchacza, onDispose gwarantowanie anuluje subskrypcję przy każdym scenariuszu zakończenia komponentu.
Szczególnie krytyczne jest to dla LazyColumn i LazyGrid, gdzie elementy są stale tworzone i niszczone podczas przewijania. Bez DisposableEffect każdy element, który znika z obszaru widoczności, pozostawiałby aktywną subskrypcję. Z DisposableEffect onDispose jest wywoływany dla każdego usuniętego elementu, gwarantując, że zasoby są zwalniane natychmiast po opuszczeniu ekranu przez element.
Według Android Performance Patterns (Google, 2025), użycie DisposableEffect dla wszystkich natywnych subskrypcji zmniejsza liczbę wycieków pamięci w aplikacjach Compose o 60–70% w porównaniu z ręcznym zarządzaniem przez callbacki cyklu życia. System sam śledzi moment opuszczenia kompozycji i gwarantuje wykonanie onDispose nawet przy awaryjnym zamknięciu ekranu.
| Zasób | Co robi DisposableEffect | Bez DisposableEffect |
|---|---|---|
| BroadcastReceiver | register + onDispose → unregister | Receiver pozostaje aktywny |
| SensorManager | registerListener + onDispose → unregister | Czujnik nadal wysyła dane |
| Observable (nie Flow) | subscribe + onDispose → unsubscribe | Callback przechowuje referencję |
| TextureView / SurfaceView | setCallback + onDispose → removeCallback | Wyciek callbacka |
| Socket / Channel | open + onDispose → close | Połączenie pozostaje otwarte |
Jeden z najbardziej obrazowych przykładów użycia DisposableEffect — praca z sensorami urządzenia (akcelerometr, żyroskop, magnetometr). Sensory wymagają obowiązkowego anulowania rejestracji przy zakończeniu pracy, w przeciwnym razie nadal pobierają energię z baterii i wysyłają dane nawet po zamknięciu ekranu.
Praktyczny przykład: aplikacja do pomiaru kąta nachylenia. DisposableEffect(Unit) rejestruje słuchacz akcelerometru przy pojawieniu się komponentu i anuluje rejestrację w onDispose. Dane z sensora są przekazywane do stanu przez mutableStateOf, co automatycznie aktualizuje UI. Jeśli ekran jest przewijany w LazyColumn i element znika, onDispose zadziała natychmiast — sensor przestaje wysyłać dane dla tego elementu.
Przy zmianie typu sensora (np. z akcelerometru na żyroskop) klucz sensorType się zmienia, onDispose anuluje starą subskrypcję, a nowy blok DisposableEffect rejestruje nowy sensor. Bez kluczy trzeba by ręcznie sprawdzać, który sensor był zarejestrowany wcześniej i wywoływać unregisterListener z odpowiednim listenerem — co grozi błędami.
@Composable
fun SensorReadingScreen(sensorType: Int) {
val context = LocalContext.current
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
var sensorValue by remember { mutableStateOf(0f) }
DisposableEffect(sensorType) {
val sensor = sensorManager.getDefaultSensor(sensorType)
val listener = SensorEventListener { event, _ ->
sensorValue = event.values[0]
}
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
onDispose {
sensorManager.unregisterListener(listener)
}
}
Text("Wartość: $sensorValue")
}
BroadcastReceiver to klasyczny przykład API wymagający obowiązkowej pary register / unregister. W aplikacji Compose DisposableEffect idealnie nadaje się do rejestracji odbiornika na czas życia konkretnego ekranu. Przy wejściu na ekran rejestrowany jest BroadcastReceiver z odpowiednim IntentFilter, przy wyjściu — automatycznie anulowany w onDispose.
Typowy scenariusz — monitorowanie stanu sieci. DisposableEffect rejestruje odbiornik na ConnectivityManager, który powiadamia o zmianach połączenia sieciowego. Przy zmianie statusu (WiFi / dane mobilne / brak sieci) aktualizowany jest stan composable, a UI wyświetla odpowiedni wskaźnik. Gdy ekran jest zamykany, onDispose gwarantowanie anuluje rejestrację — nawet jeśli aplikacja przechodzi do tła.
Dla odbiorników z ContextCompat.registerReceiver i flagą RECEIVER_EXPORTED / RECEIVER_NOT_EXPORTED (Android 14+) użycie DisposableEffect staje się obowiązkowe, ponieważ system wymaga jawnego określenia zakresu działania odbiornika. DisposableEffect gwarantuje, że zakres działania jest ograniczony do czasu życia ekranu, co jest zgodne z wymogami bezpieczeństwa nowych wersji Androida.
@Composable
fun NetworkStatusBanner() {
val context = LocalContext.current
var isConnected by remember { mutableStateOf(true) }
DisposableEffect(Unit) {
val receiver = BroadcastReceiver { _, _ ->
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
isConnected = cm.getActiveNetwork() != null
}
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).let { filter ->
context.registerReceiver(receiver, filter)
}
onDispose {
context.unregisterReceiver(receiver)
}
}
if (!isConnected) { ... }
}
Pierwszy krytyczny błąd — brak wywołania onDispose. Kod wewnątrz bloku DisposableEffect musi wywołać onDispose, w przeciwnym razie wystąpi błąd kompilacji. Jednak programiści czasami próbują to obejść, umieszczając onDispose w warunku: if (condition) { onDispose { ... } }. Taki kod się skompiluje, ale onDispose nie zostanie zarejestrowany, jeśli warunek nie zostanie spełniony — zasób nigdy nie zostanie zwolniony.
Drugi błąd — użycie DisposableEffect do operacji asynchronicznych. Ponieważ DisposableEffect jest synchroniczny, wewnątrz niego nie można pisać wywołań delay() ani await(). Jeśli potrzebna jest asynchroniczna inicjalizacja z późniejszym czyszczeniem, użyj kombinacji LaunchedEffect (do ładowania danych) i DisposableEffect (do konfiguracji/czyszczenia zasobów natywnych) lub użyj osobnego mechanizmu z rememberCoroutineScope.
Trzeci błąd — tworzenie nowych obiektów wewnątrz DisposableEffect bez remember. Jeśli wewnątrz efektu tworzone są obiekty (sensor, listener, odbiornik) przy każdym wywołaniu, a klucze często się zmieniają, prowadzi to do nadmiernego tworzenia obiektów i zbierania śmieci. Lepiej wynieść tworzenie obiektów do remember lub remember { ... } poza DisposableEffect, a wewnątrz efektu tylko rejestrować i anulować.
Często zadawane pytania
DisposableEffect działa synchronicznie i udostępnia onDispose do jawnego czyszczenia zasobów. LaunchedEffect działa asynchronicznie w korutynie i automatycznie ją anuluje przy zmianie klucza lub opuszczeniu kompozycji. Jeśli zasób wymaga wywołania metody cleanup (close, unregister, dispose) — użyj DisposableEffect. Jeśli operacja jest funkcją suspend — użyj LaunchedEffect.
Tak, onDispose jest obowiązkowy — kompilator Kotlina wymaga jego wywołania wewnątrz bloku DisposableEffect. Jeśli nie wywołasz onDispose, kod się nie skompiluje. Zostało to celowo zaprojektowane, aby zapobiec roztargnieniu programistów i zagwarantować, że każdy otwarty zasób zostanie zamknięty przy opuszczeniu kompozycji.
Użyj try-catch wewnątrz bloku DisposableEffect. Jeśli rejestracja zasobu może zgłosić wyjątek (np. sensor nie został znaleziony), opakuj ją w try i obsłuż błąd w UI przez osobny stan. onDispose powinien być wywołany niezależnie od powodzenia inicjalizacji — umieść go w bloku finally lub na końcu sekcji try.
Nie jest zalecane. Dla Flow lepiej użyć LaunchedEffect z collectLatest lub metody .collectAsState() z Lifecycle.repeatOnLifecycle. DisposableEffect nie obsługuje funkcji suspend, więc subskrypcja Flow wewnątrz niego wymagałaby uruchomienia osobnej korutyny przez CoroutineScope, co komplikuje kod i zwiększa ryzyko wycieków.
Nie ma ograniczeń, ale zaleca się grupowanie powiązanych zasobów w jeden DisposableEffect z wieloma operacjami wewnątrz i jednym onDispose. Jeśli zasoby są niezależne (np. sensor i BroadcastReceiver), lepiej podzielić je na osobne DisposableEffect z różnymi kluczami — upraszcza to debugowanie i zapobiega niepożądanemu odtwarzaniu wszystkich zasobów przy zmianie jednego klucza.
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ż