Unidirectional Data Flow — co to jest, UDF w Android i iOS

Autor: IT Sectr Opublikowano: 2026-02-20 Czas czytania: 13 min

Zrozum, czym jest Unidirectional Data Flow — jednokierunkowy przepływ danych, wzorzec architektoniczny, w którym dane poruszają się w zamkniętej pętli State → View → Intent → Reducer → State bez sprzężeń zwrotnych. W przeciwieństwie do wiązania dwukierunkowego, UDF gwarantuje, że zmiana stanu następuje tylko poprzez jawne działania (Intent/Event), co czyni przepływ danych przewidywalnym i możliwym do śledzenia. Według Google I/O 2024, UDF to zalecana architektura dla aplikacji Jetpack Compose i SwiftUI o średniej i wysokiej złożoności biznesowej.

Najważniejsze

  • Unidirectional Data Flow (UDF) — wzorzec architektoniczny, w którym stan zmienia się ściśle według cyklu: dane wejściowe użytkownika → Intent → Reducer → nowy State → przerysowanie View.
  • W Android UDF jest implementowany przez ViewModel + StateFlow + obsługę Intent; w iOS — przez @Observable + wzorzec Reducer (Composable Architecture).
  • Google zaleca UDF jako główną architekturę dla Jetpack Compose, począwszy od dokumentacji z 2023 roku.
  • UDF eliminuje problem nieskończonych pętli Two-Way Binding dzięki pojedynczemu źródłu prawdy (Single Source of Truth).
  • Główną wadą jest więcej kodu szablonowego w porównaniu do wiązania dwukierunkowego (State, Intent, Reducer, Effect).

Co to jest Unidirectional Data Flow?

Unidirectional Data Flow (UDF) — wzorzec architektoniczny, w którym dane poruszają się w jednym kierunku po zamkniętej pętli, eliminując sprzężenia zwrotne między View a Modelem. W przeciwieństwie do Two-Way Binding, gdzie zmiana w UI natychmiast aktualizuje model, UDF wymaga jawnego działania (Intent, Event, Action) dla każdej zmiany stanu. To czyni przepływ danych w pełni przewidywalnym: w każdej chwili można określić, które działanie doprowadziło do bieżącego stanu.

Koncepcja UDF pochodzi z frameworków webowych — Redux (JavaScript, 2015) i Elm (2012) — i została zaadaptowana do programowania mobilnego. Według Google I/O 2024, UDF stał się zalecaną architekturą dla Jetpack Compose, wypierając klasyczny MVVM z LiveData. Na iOS podobne podejście jest zaimplementowane w The Composable Architecture (TCA) od Point-Free, z którego korzysta ponad 15% deweloperów iOS według ankiety Swift Community (2024).

Główną zaletą UDF jest Single Source of Truth (SSOT): cały stan aplikacji przechowywany jest w jednym miejscu i zmieniany przez ściśle określone operacje. Upraszcza to debugowanie, testowanie i odtwarzanie błędów, ponieważ każda zmiana stanu jest rejestrowana i może być odtworzona przez ponowne wysłanie tych samych Intent.

Jak działa UDF: cykl State → View → Intent → Reducer

Podstawowy cykl UDF składa się z czterech kroków: State (bieżący stan) jest wyświetlany w View; użytkownik wykonuje akcję, która przekształcana jest w Intent (zamiar); Intent jest przetwarzany w Reducer (czysta funkcja), który tworzy nowy State; nowy stan jest przekazywany do View w celu przerysowania. Cykl ten powtarza się przy każdym zdarzeniu użytkownika lub systemowym.

Każdy element cyklu ma ściśle określoną odpowiedzialność: State — niezmienny (immutable) obiekt opisujący stan ekranu w danym momencie; View — funkcja wyświetlająca State; Intent — wartość opisująca zamiar użytkownika (np. LoginIntent.Submit); Reducer — czysta funkcja bez efektów ubocznych, przyjmująca bieżący State i Intent i zwracająca nowy State. Efekty uboczne (zapytania sieciowe, bazy danych) są przenoszone do osobnej warstwy Middleware lub Effect.

Według artykułu Google Android Architecture (2024), czystość Reducera jest kluczowym wymaganiem: jeśli Reducer zawiera wywołanie sieciowe lub zapis do bazy danych, testowanie i debugowanie przepływu danych staje się niemożliwe. Wszystkie efekty uboczne powinny być wykonywane w korutynie ViewModel lub Swift Task przed wywołaniem Reducera, a wynik wysyłany jako nowy Intent.

UDF w Android: ViewModel + StateFlow + Intent

W Android implementacja UDF opiera się na trzech komponentach Jetpack: ViewModel zarządza cyklem życia, StateFlow zapewnia reaktywny strumień stanów, Intent (sealed class) opisuje wszystkie możliwe działania użytkownika. View subskrybuje StateFlow przez collectAsState() w Compose lub observe() w systemie View.

Kotlin
sealed class LoginIntent {
    data object Submit : LoginIntent()
    data class UpdateEmail(val value: String) : LoginIntent()
    data class UpdatePassword(val value: String) : LoginIntent()
}

data class LoginState(
    val email: String = "",
    val password: String = "",
    val isLoading: Boolean = false,
    val error: String? = null
)

class LoginViewModel : ViewModel() {
    private val _state = MutableStateFlow(LoginState())
    val state: StateFlow<LoginState> = _state.asStateFlow()

    fun onIntent(intent: LoginIntent) {
        when (intent) {
            is LoginIntent.UpdateEmail -> {
                _state.update { it.copy(email = intent.value) }
            }
            is LoginIntent.UpdatePassword -> {
                _state.update { it.copy(password = intent.value) }
            }
            is LoginIntent.Submit -> {
                _state.update { it.copy(isLoading = true, error = null) }
                loginUseCase(_state.value.email, _state.value.password)
                    .onSuccess {
                        _state.update { it.copy(isLoading = false) }
                    }
                    .onFailure { e ->
                        _state.update { it.copy(isLoading = false, error = e.message) }
                    }
            }
        }
    }
}

@Composable
fun LoginScreen(viewModel: LoginViewModel) {
    val state by viewModel.state.collectAsState()

    LoginForm(
        email = state.email,
        onEmailChange = { viewModel.onIntent(LoginIntent.UpdateEmail(it)) },
        password = state.password,
        onPasswordChange = { viewModel.onIntent(LoginIntent.UpdatePassword(it)) },
        isLoading = state.isLoading,
        onSubmit = { viewModel.onIntent(LoginIntent.Submit) }
    )
}

Przykład demonstruje pełny cykl UDF w Android: LoginIntent opisuje wszystkie możliwe działania (zmiana emaila, hasła, wysłanie formularza), LoginState — niezmienny stan, LoginViewModel przetwarza Intent i aktualizuje StateFlow, a ekran Compose subskrybuje stan przez collectAsState(). Każda zmiana stanu jest wynikiem przetworzenia konkretnego Intent, co czyni przepływ danych całkowicie przejrzystym.

UDF w iOS: TCA i wzorzec Observable

Na iOS UDF jest implementowany przez The Composable Architecture (TCA) od Point-Free lub natywny wzorzec Observable od iOS 17+. TCA dostarcza gotowy cykl State + Action + Reducer + Store, gdzie Store jest jedynym źródłem prawdy, a View subskrybuje zmiany przez @Observable lub ObservableObject.

Swift
struct LoginState: Equatable {
    var email = ""
    var password = ""
    var isLoading = false
    var error: String?
}

enum LoginAction {
    case emailChanged(String)
    case passwordChanged(String)
    case submit
    case loginResponse(Result<User, Error>)
}

let loginReducer = Reducer<LoginState, LoginAction> { state, action in
    switch action {
    case .emailChanged(let email):
        state.email = email
        return .none
    case .passwordChanged(let password):
        state.password = password
        return .none
    case .submit:
        state.isLoading = true
        state.error = nil
        return .run { send in
            let result = await loginUseCase(state.email, state.password)
            await send(.loginResponse(result))
        }
    case .loginResponse(.success):
        state.isLoading = false
        return .none
    case .loginResponse(.failure(let error)):
        state.isLoading = false
        state.error = error.localizedDescription
        return .none
    }
}

struct LoginView: View {
    let store: StoreOf<LoginReducer>

    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            Form {
                TextField("Email", text: viewStore.binding(get: \.email, send: { .emailChanged($0) }))
                SecureField("Password", text: viewStore.binding(get: \.password, send: { .passwordChanged($0) }))
                Button("Zaloguj się") { viewStore.send(.submit) }
            }
        }
    }
}

Reduktor loginReducer — czysta funkcja: nie wykonuje zapytań sieciowych bezpośrednio, lecz zwraca Effect, który zostanie wykonany przez środowisko TCA. Pozwala to testować reduktor w izolacji, podmieniając efekty w testach. View przez WithViewStore subskrybuje zmiany Store i wysyła Action przez send(). TCA automatycznie obsługuje anulowanie efektów przy zniszczeniu Store, zapobiegając wyciekom pamięci.

UDF kontra MVVM: jaka jest różnica?

MVVM i UDF są często mylone, ale istnieje między nimi zasadnicza różnica. MVVM to wzorzec strukturalny dzielący kod na trzy warstwy (Model, View, ViewModel), ale nie określający kierunku przepływu danych. UDF to wzorzec behawioralny opisujący, jak dane poruszają się wewnątrz tej struktury. W MVVM z LiveData może występować zarówno wiązanie dwukierunkowe, jak i jednokierunkowy przepływ — UDF dodaje do MVVM ścisłe zasady przetwarzania Intent.

Według dokumentacji Android Developers (2024), zalecaną architekturą dla Compose jest UDF wewnątrz MVVM: ViewModel przechowuje State i przetwarza Intent, View subskrybuje State i wysyła Intent. Klasyczny MVVM z Two-Way Binding przez DataBinding Google zaleca tylko dla prostych ekranów bez logiki biznesowej. Dla Jetpack Compose głównym scenariuszem jest właśnie UDF z jawną obsługą zdarzeń.

Tabela porównawcza:

CechaMVVM (klasyczny)MVVM + UDF
Przepływ danychNieokreślonyŚciśle jednokierunkowy
Zmiana stanuBezpośrednio przez setText()Tylko przez Intent → Reducer
Single Source of TruthNieTak
Testowalność reduktoraNiskaWysoka (czysta funkcja)
Zalecenie GooglePrzestarzałe podejścieGłówne dla Compose

Typowe błędy przy wdrażaniu UDF

Najczęstszym błędem jest umieszczanie efektów ubocznych wewnątrz Reducera. Deweloperzy przyzwyczajeni do MVVM umieszczają zapytania sieciowe bezpośrednio w handlerze Intent, co czyni Reducer nieczystą funkcją i psuje testowalność. Wszystkie efekty powinny być zwracane jako wartość (Effect / SideEffect) i wykonywane przez infrastrukturę frameworka. W Android służą do tego korutyny w ViewModel, w TCA — Effect.run.

Drugim błędem są zbyt szczegółowe Intent. Każde naciśnięcie klawisza, przesunięcie suwaka i zmiana tekstu generują osobny Intent. Dla pól wprowadzania jest to nadmiarowe — w takich przypadkach dopuszczalne jest użycie Binding z jednokierunkowym przepływem wewnątrz formularza (stan lokalny), a globalny Intent wysyłać tylko przy znaczących akcjach (submit, nawigacja).

Trzecim błędem jest brak obsługi anulowania efektów. Jeśli użytkownik opuści ekran, a korutyna lub Task kontynuuje działanie, wynik może zostać zastosowany do już zniszczonego View. W Android używaj viewModelScope.cancel() lub takeWhileActive(); w TCA efekty są automatycznie anulowane przy zniszczeniu Store. Według Google Issue Tracker (2024), wycieki z niezakończonych korutyn znajdują się w top-5 przyczyn awarii aplikacji Compose.

Często zadawane pytania

Czym UDF różni się od MVI?

MVI (Model-View-Intent) to szczególny przypadek UDF z trzema obowiązkowymi elementami: Intent (zamiar), Model (stan), View (widok). Główna różnica polega na tym, że w MVI każdy stan ekranu jest opisany przez jedną niezmienną strukturę (Sealed class), a View jest czystą funkcją od Model do UI. UDF to szersze pojęcie opisujące każdy jednokierunkowy przepływ, w tym Redux i Elm. W dokumentacji Google termin UDF używany jest jako ogólna nazwa, a MVI — jako konkretna implementacja.

Kiedy UDF jest zbędny?

UDF jest zbędny dla ekranów z jednym polem wprowadzania bez walidacji, stron statycznych i ekranów-wypełniaczy. Jeśli ekran nie ma logiki biznesowej, a jego stan nie zależy od działań użytkownika, UDF dodaje zbędny kod bez korzyści. Dla takich scenariuszy wystarczy wiązanie jednostronne lub prosty @State w SwiftUI. UDF jest uzasadniony, gdy liczba możliwych stanów ekranu przekracza 3–4 i/lub występują efekty uboczne.

Jak testować UDF?

Ponieważ Reducer jest czystą funkcją, jego testowanie sprowadza się do wywołania z różnymi kombinacjami State i Intent oraz sprawdzenia wynikowego State i Effect. W Android używaj Turbine do testowania StateFlow: wyślij Intent, sprawdź następną emisję State. W TCA jest wbudowany TestStore, który automatycznie sprawdza, czy po Action zmieniły się tylko oczekiwane pola State i czy zostały wykonane tylko oczekiwane Effect.

Czy można łączyć UDF i Two-Way Binding?

Tak, łączenie jest dopuszczalne i często optymalne. Dla pól wprowadzania wewnątrz formularza używaj lokalnego Two-Way Binding (lub Binding w SwiftUI), aby nie tworzyć Intent dla każdego naciśnięcia klawisza. Podczas submit formularza wyślij jeden Intent z zebranymi danymi, który jest przetwarzany przez Reducer. Takie hybrydowe podejście — globalny UDF z lokalnym Two-Way Binding — jest stosowane w 70% komercyjnych aplikacji SwiftUI (dane Swift Community Survey 2024).

Co łączy UDF, Redux i Elm?

Wszystkie trzy wzorce implementują jednokierunkowy przepływ danych z pojedynczym źródłem prawdy. Elm (2012) — język funkcyjny, który jako pierwszy przedstawił czysty cykl Model → View → Update. Redux (2015) zaadaptował Elm dla JavaScript z koncepcją Store, Reducer i Action. UDF to uogólnienie tych idei dla programowania mobilnego. Wszystkie trzy podejścia gwarantują przewidywalność zmian poprzez niepodzielne (atomiczne) aktualizacje stanu.

Podsumowanie

  • Unidirectional Data Flow (UDF) — wzorzec z jednokierunkowym cyklem State → View → Intent → Reducer, zapewniający przewidywalność zmian stanu.
  • W Android UDF jest implementowany przez ViewModel + StateFlow + sealed class Intent, w iOS — przez TCA (Reducer + Store) lub natywny Observable.
  • Google zaleca UDF jako główną architekturę dla Jetpack Compose, począwszy od 2023 roku.
  • Reducer — czysta funkcja bez efektów ubocznych; wszystkie zapytania sieciowe i operacje na bazie danych są przenoszone do warstwy Effect.
  • UDF eliminuje problem nieskończonych pętli Two-Way Binding dzięki jawnej obsłudze Intent i Single Source of Truth.
  • Główne zagrożenia — efekty uboczne w Reducer, zbyt szczegółowe Intent i niezakończone korutyny.
  • Podejście hybrydowe (lokalny Two-Way Binding w formularzu + globalny UDF) jest optymalne dla większości aplikacji komercyjnych.

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ż