State Hoisting: podnoszenie stanu i jednokierunkowy przepływ w Compose

Autor: IT Sectr Opublikowano: 2026-06-28 Czas czytania: 8 min

State Hoisting — to wzorzec w Jetpack Compose, w którym stan jest wynoszony z funkcji podrzędnej Composable do nadrzędnej, a funkcja podrzędna otrzymuje dane przez parametry i informuje o zmianach poprzez callbacki. Jest to realizacja zasady jednokierunkowego przepływu danych (UDF), w której stan jest przenoszony w górę, a zdarzenia w dół. Według Google Android Developers, 2026, State Hoisting sprawia, że komponenty są wielokrotnego użytku, testowalne i przewidywalne.

Najważniejsze

  • State Hoisting przenosi stan z komponentu podrzędnego do nadrzędnego
  • UDF (Unidirectional Data Flow) — stan płynie w dół, zdarzenia w górę
  • Parametry komponentu podrzędnego: wartość (T) + lambda (T) -> Unit
  • Wielokrotne użycie — podniesiony stan umożliwia użycie tej samej funkcji z różnymi źródłami
  • Testowanie — State Hoisting upraszcza testy jednostkowe, izolując logikę od UI

Czym jest State Hoisting w Jetpack Compose

State Hoisting (podnoszenie stanu) — to wzorzec, w którym funkcja Composable nie posiada stanu, ale otrzymuje go z zewnątrz. Zamiast var wewnątrz funkcji stosuje się dwa parametry: wartość do wyświetlenia i lambdę-callback do obsługi zmian. Technicznie oznacza to, że komponent podrzędny staje się stateless (nie ma własnego stanu), a nadrzędny — stateful (posiada stan).

Przykład: komponent TextField z Material3 nie przechowuje wprowadzonego tekstu wewnątrz siebie. Przyjmuje value: String i onValueChange: (String) -> Unit. Nadrzędny, wywołujący TextField, deklaruje var value by remember { mutableStateOf("") } i przekazuje value oraz onValueChange. To klasyczny State Hoisting: TextField — komponent głupi (tylko wyświetla i informuje o wprowadzaniu), nadrzędny — mądry (posiada stan).

Stateless vs Stateful: Komponent stateless jest łatwiejszy do testowania — nie zależy od wewnętrznego stanu, jego zachowanie jest w pełni określone przez parametry wejściowe. Komponent stateful jest wygodny do szybkiego prototypowania, ale trudniejszy do wielokrotnego użycia — jest sztywno powiązany z jednym źródłem danych. State Hoisting daje wybór: każdy komponent można uczynić stateless, przenosząc stan wyżej.

Jednokierunkowy przepływ danych (UDF) a State Hoisting

UDF (Unidirectional Data Flow) — to architektoniczna zasada, w której dane płyną w jednym kierunku: od źródła prawdy (ViewModel lub nadrzędny Composable) do UI, a zdarzenia — w przeciwnym kierunku. State Hoisting to realizacja UDF na poziomie poszczególnych komponentów. Zamiast każdego komponentu samodzielnie decydującego, kiedy i jak zmienić swój stan, informuje on nadrzędnego o zdarzeniu, a nadrzędny decyduje, jak zmienić stan.

Zalety UDF: przewidywalność — stan zmienia się tylko w jednym miejscu, co eliminuje race conditions; traceability — na podstawie stosu wywołań można odtworzyć łańcuch zmian; testowanie — logikę stateful można wydzielić do osobnej klasy i testować bez UI. W dużych projektach UDF w połączeniu z State Hoisting to standard de facto.

Pojedyncze źródło prawdy (Single Source of Truth) — kolejna zasada towarzysząca UDF. Każdy fragment stanu ma dokładnie jedno źródło. Jeśli dwa komponenty używają tego samego stanu, źródło musi być wspólne (na poziomie ViewModel lub wspólnego nadrzędnego). State Hoisting gwarantuje, że źródło znajduje się wyżej w hierarchii i nie dochodzi do powielania stanu.

KierunekCo jest przekazywaneJak zaimplementowane
W dół (nadrzędny → podrzędny)Wartość do wyświetleniaParametr value: T
W górę (podrzędny → nadrzędny)Zdarzenie o zmianieParametr onValueChange: (T) -> Unit

Zasady State Hoisting: kiedy i jak podnosić stan

Główna zasada: stan powinien być podniesiony na minimalny możliwy poziom, wystarczający dla wszystkich komponentów, które go potrzebują. Jeśli stan jest używany tylko wewnątrz jednego komponentu — pozostaw go lokalnym. Jeśli dwa sąsiednie komponenty potrzebują tego samego stanu — podnieś do wspólnego nadrzędnego. Jeśli stan jest potrzebny na całym ekranie — podnieś do ViewModel.

Zasada minimalnego podnoszenia zapobiega niepotrzebnej złożoności. Nie ma sensu podnosić stan pola tekstowego do ViewModel, jeśli jest używany tylko wewnątrz jednego ekranu i nie jest zachowywany przy odtworzeniu Activity. Użyj rememberSaveable na poziomie nadrzędnego ekranu, a nie ViewModel, dla stanu UI, który ma przetrwać obrót ekranu, ale nie jest potrzebny logice biznesowej.

Kiedy podnosić do ViewModel: jeśli stan musi być zachowany przy odtworzeniu Activity, jeśli jest potrzebny wielu ekranom, jeśli zmiana stanu uruchamia logikę biznesową (zapytania sieciowe, BD). State Hoisting na poziomie ViewModel to typowy wzorzec w architekturze MVVM, gdzie warstwa UI jest stateless, a ViewModel — stateful.

kotlin
    // ❌ Źle: komponent posiada własny stan
@Composable
fun BadTextField(label: String) {
    var text by remember { mutableStateOf("") }
    TextField(value = text, onValueChange = { text = it }, label = { Text(label) })
}

    // ✅ Dobrze: State Hoisting — stan w rodzicu
@Composable
fun GoodTextField(value: String, onValueChange: (String) -> Unit, label: String) {
    TextField(value = value, onValueChange = onValueChange, label = { Text(label) })
}

    // Zastosowanie: rodzic posiada stan
@Composable
fun Form() {
    var name by rememberSaveable { mutableStateOf("") }
    GoodTextField(value = name, onValueChange = { name = it }, label = "Name")
}

Przykłady State Hoisting w rzeczywistych komponentach

Rozważmy ekran logowania, na którym są dwa pola (email, hasło) i przycisk. Wszystkie trzy komponenty otrzymują stan przez State Hoisting: email i hasło są zarządzane przez nadrzędnego, przycisk otrzymuje status enabled jako wartość.

kotlin
    // State Hoisting na poziomie ekranu
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
    val uiState by viewModel.uiState.collectAsState()

    Column(modifier = Modifier.padding(16.dp)) {
        // Pole Email — State Hoisting przez lambdę
        EmailField(
            email = uiState.email,
            onEmailChange = { viewModel.onEmailChanged(it) }
        )

        // Pole hasła — podobnie
        PasswordField(
            password = uiState.password,
            onPasswordChange = { viewModel.onPasswordChanged(it) }
        )

        // Przycisk — otrzymuje tylko enabled (tylko do odczytu)
        LoginButton(enabled = uiState.isFormValid, onClick = viewModel::login)
    }
}

// Komponent bezstanowy: otrzymuje email + callback
@Composable
fun EmailField(email: String, onEmailChange: (String) -> Unit) {
    OutlinedTextField(
        value = email,
        onValueChange = onEmailChange,
        label = { Text("Email") },
        singleLine = true
    )
}

// Komponent przycisku bezstanowy
@Composable
fun LoginButton(enabled: Boolean, onClick: () -> Unit) {
    Button(onClick = onClick, enabled = enabled) {
        Text("Zaloguj")
    }
}

EmailField i PasswordField — całkowicie stateless. Można je wielokrotnie używać na dowolnym ekranie, podłączając do dowolnego źródła danych. LoginButton otrzymuje enabled jako read-only — to kolejna forma State Hoisting, gdzie stan nie jest podnoszony (przycisk nie może sam się włączyć), ale jest przekazywany gotowy. Takie podejście daje maksymalną elastyczność przy minimalnym powiązaniu komponentów.

State Hoisting vs stan lokalny: kryteria wyboru

Nie każdy stan trzeba podnosić. Stan lokalny (State wewnątrz Composable) jest uzasadniony, gdy: dane są potrzebne tylko wewnątrz jednego komponentu, nie wpływają na sąsiednie elementy, nie muszą przetrwać rekompozycji konkretnej sekcji. Na przykład stan animacji, focus pola wprowadzania, bieżąca pozycja scrolla — warto trzymać lokalnie.

Kiedy State Hoisting jest niezbędny: stan jest używany przez kilka podrzędnych komponentów; zmiana w jednym podrzędnym musi odzwierciedlać się w drugim; trzeba testować logikę zmiany stanu oddzielnie od UI; stan musi być zachowany przy odtworzeniu Activity. W tych przypadkach stan lokalny tworzy powielenie i niespójność danych.

Podejście hybrydowe: trzymaj minimalny stan lokalnie, resztę podnoś. Zasada Compose: „podnoś stan tak wysoko, jak to konieczne, i tak nisko, jak to możliwe”. W praktyce oznacza to zacząć od lokalnego remember, a dopiero gdy pojawi się potrzeba dostępu z innego komponentu — podnieś poziom. Nie stosuj State Hoisting prewencyjnie — to komplikuje kod bez potrzeby.

Często zadawane pytania

Czym różni się State Hoisting od ViewModel?

State Hoisting — to wzorzec na poziomie komponentów UI. ViewModel — to warstwa architektoniczna dla logiki biznesowej. State Hoisting może podnosić stan na poziom nadrzędnego Composable, na poziom ekranu lub do ViewModel. ViewModel — to najwyższy punkt podnoszenia dla stanu, który ma przetrwać odtworzenie Activity.

Jak testować komponent z State Hoisting?

Komponent stateless testuje się poprzez proste przekazanie wartości. Wywołaj Composable z odpowiednimi parametrami i sprawdź wyświetlanie przez ComposeTestRule. Zmiana stanu jest testowana na poziomie nadrzędnego lub ViewModel — oddzielnie od UI. To znacząco upraszcza testy: nie trzeba symulować rekompozycji wewnątrz komponentu.

Czy można podnosić State tylko do odczytu?

Tak, to częsta praktyka. Jeśli komponent ma tylko wyświetlać dane bez możliwości ich zmiany — przekazuj State<T> (tylko do odczytu). Komponent będzie subskrybował zmiany, ale nie będzie mógł ich inicjować. To wzmacnia enkapsulację i chroni dane przed niepożądanymi mutacjami.

Co jeśli trzeba podnieść stan na 3+ poziomach zagnieżdżenia?

Do głębokiego przekazywania używaj CompositionLocal lub przekazywania przez parametry nadrzędnego Composable. Jeśli stan jest potrzebny na całym ekranie — wyodrębnij go do ViewModel i użyj collectAsState(). Przekazywanie przez 5+ poziomów — oznaka nieprawidłowej architektury; przejrzyj hierarchię komponentów.

Czy State Hoisting wpływa na wydajność?

State Hoisting może nieznacznie zwiększyć liczbę rekompozycji, ponieważ zmiana w nadrzędnym może rekomponować wszystkie podrzędne. Używaj derivedStateOf do filtrowania zmian i keys w LazyColumn do punktowych aktualizacji. W większości scenariuszy narzut State Hoisting jest pomijalnie mały w porównaniu z korzyściami z utrzymywalności.

Podsumowanie

  • State Hoisting przenosi stan do nadrzędnego komponentu, czyniąc podrzędny stateless
  • UDF gwarantuje jednokierunkowy przepływ danych: stan w dół, zdarzenia w górę
  • Wielokrotne użycie — komponenty stateless można podłączać do dowolnych źródeł danych
  • Testowanie — testy UI sprawdzają tylko wyświetlanie, logika testowana osobno
  • Minimalne podnoszenie — podnoś dokładnie tak bardzo, jak to potrzebne
  • ViewModel — najwyższy punkt podnoszenia dla stanu z logiką biznesową
  • Zalecenie: zaczynaj od lokalnego remember, podnoś tylko w razie potrzeby

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.

Omów projekt

Przeczytaj również