Unidirectional Data Flow — mi ez, UDF Android és iOS rendszeren

Szerző: IT Sectr Megjelenés: 2026-02-20 Olvasási idő: 13 perc

Értse meg, mi az Unidirectional Data Flow — egyirányú adatfolyam, egy architektúraminta, amelyben az adatok egy zárt ciklusban mozognak State → View → Intent → Reducer → State visszacsatolás nélkül. A kétirányú kötéssel ellentétben az UDF garantálja, hogy az állapotváltozás csak explicit műveleteken (Intent/Event) keresztül történik, ami az adatfolyamot kiszámíthatóvá és nyomon követhetővé teszi. A Google I/O 2024 szerint az UDF az ajánlott architektúra közepes és magas üzleti logikai komplexitású Jetpack Compose és SwiftUI alkalmazásokhoz.

Főbb pontok

  • Unidirectional Data Flow (UDF) — architektúraminta, amelyben az állapot szigorúan a ciklus szerint változik: felhasználói bemenet → Intent → Reducer → új State → View újrarajzolása.
  • Androidon az UDF ViewModel + StateFlow + Intent feldolgozáson keresztül valósul meg; iOS-en — @Observable + Reducer minta (Composable Architecture) révén.
  • A Google 2023-as dokumentációtól kezdve az UDF-et ajánlja fő architektúraként a Jetpack Compose számára.
  • Az UDF megszünteti a Two-Way Binding végtelen ciklusainak problémáját az egyetlen igazságforrás (Single Source of Truth) révén.
  • A fő hátrány — több sablonkód a kétirányú kötéshez képest (State, Intent, Reducer, Effect).

Mi az Unidirectional Data Flow?

Unidirectional Data Flow (UDF) — egy architektúraminta, amelyben az adatok egy irányban mozognak egy zárt ciklus mentén, kizárva a visszacsatolást a View és Model között. A Two-Way Binding-gal ellentétben, ahol a UI változása azonnal frissíti a modellt, az UDF explicit műveletet (Intent, Event, Action) igényel minden állapotváltozáshoz. Ez az adatfolyamot teljesen kiszámíthatóvá teszi: bármely pillanatban meghatározható, hogy melyik művelet vezetett az aktuális állapothoz.

Az UDF koncepciója webes keretrendszerekből — Redux (JavaScript, 2015) és Elm (2012) — származik, és mobilfejlesztésre adaptálták. A Google I/O 2024 szerint az UDF a Jetpack Compose ajánlott architektúrájává vált, kiszorítva a klasszikus MVVM-et LiveData-val. iOS-en hasonló megközelítés valósul meg a Point-Free The Composable Architecture (TCA) keretrendszerében, amelyet a Swift Community felmérése (2024) szerint az iOS-fejlesztők több mint 15%-a használ.

Az UDF fő előnye — Single Source of Truth (SSOT): az alkalmazás teljes állapota egy helyen tárolódik és szigorúan meghatározott műveleteken keresztül változik. Ez egyszerűsíti a hibakeresést, tesztelést és a hibák reprodukálását, mivel minden állapotváltozás naplózásra kerül, és ugyanazon Intent-ek újraküldésével reprodukálható.

Hogyan működik az UDF: State → View → Intent → Reducer ciklus

Az alap UDF ciklus négy lépésből áll: State (aktuális állapot) megjelenik a View-ban; a felhasználó végrehajt egy műveletet, amely Intent-té (szándékká) alakul; az Intent feldolgozásra kerül a Reducer-ben (egy tiszta függvény), amely új State-et hoz létre; az új állapot átadásra kerül a View-nak az újrarajzoláshoz. Ez a ciklus minden felhasználói vagy rendszereseménynél megismétlődik.

A ciklus minden elemének szigorú felelőssége van: State — egy megváltoztathatatlan (immutable) objektum, amely a képernyő állapotát írja le egy adott pillanatban; View — egy függvény, amely megjeleníti a State-et; Intent — egy érték, amely leírja a felhasználó szándékát (pl. LoginIntent.Submit); Reducer — egy tiszta függvény mellékhatások nélkül, amely fogadja az aktuális State-et és Intent-et, és visszaadja az új State-et. A mellékhatások (hálózati kérések, adatbázis) külön Middleware vagy Effect rétegbe kerülnek.

A Google Android Architecture (2024) cikke szerint a Reducer tisztasága kulcsfontosságú követelmény: ha a Reducer hálózati hívást vagy adatbázis-írást tartalmaz, az adatfolyam tesztelése és hibakeresése lehetetlenné válik. Minden mellékhatást a ViewModel korutinjában vagy Swift Task-ban kell végrehajtani a Reducer meghívása előtt, az eredményt pedig új Intentként kell elküldeni.

UDF Androidon: ViewModel + StateFlow + Intent

Androidon az UDF implementációja három Jetpack komponensen alapul: ViewModel kezeli az életciklust, StateFlow reaktív állapotfolyamot biztosít, Intent (sealed class) leírja az összes lehetséges felhasználói műveletet. A View a StateFlow-ra iratkozik fel a collectAsState() segítségével Compose-ban vagy az observe()-val a View rendszerben.

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) }
    )
}

A példa a teljes UDF ciklust mutatja Androidon: LoginIntent leírja az összes lehetséges műveletet (e-mail módosítása, jelszó módosítása, űrlap elküldése), LoginState — a megváltoztathatatlan állapot, LoginViewModel feldolgozza az Intent-eket és frissíti a StateFlow-t, a Compose képernyő pedig a collectAsState()-on keresztül iratkozik fel az állapotra. Minden állapotváltozás egy adott Intent feldolgozásának eredménye, ami az adatfolyamot teljesen átláthatóvá teszi.

UDF iOS-en: TCA és Observable minta

iOS-en az UDF a The Composable Architecture (TCA) Point-Free-tól vagy a natív Observable mintán keresztül valósul meg iOS 17+-tól. A TCA egy kész State + Action + Reducer + Store ciklust biztosít, ahol a Store az egyetlen igazságforrás, a View pedig a @Observable vagy ObservableObject segítségével iratkozik fel a változásokra.

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("Bejelentkezés") { viewStore.send(.submit) }
            }
        }
    }
}

A loginReducer reduktor — tiszta függvény: nem hajt végre közvetlenül hálózati kéréseket, hanem egy Effect-et ad vissza, amelyet a TCA környezet hajt végre. Ez lehetővé teszi a reduktor elkülönített tesztelését, a hatások tesztbeli helyettesítését. A View a WithViewStore segítségével iratkozik fel a Store változásaira, és a send()-en keresztül küld Action-t. A TCA automatikusan kezeli a hatások megszakítását a Store megsemmisítésekor, megelőzve a memóriaszivárgást.

UDF kontra MVVM: mi a különbség?

MVVM és UDF gyakran összekeverik, de alapvető különbség van közöttük. Az MVVM egy strukturális minta, amely három rétegre osztja a kódot (Model, View, ViewModel), de nem határozza meg az adatfolyam irányát. Az UDF egy viselkedési minta, amely leírja, hogyan mozognak az adatok ezen a struktúrán belül. A LiveData-val ellátott MVVM-ben lehet kétirányú kötés és egyirányú folyam is — az UDF szigorú Intent-feldolgozási szabályokat ad az MVVM-hez.

Az Android Developers (2024) dokumentációja szerint a Compose ajánlott architektúrája az UDF az MVVM-en belül: a ViewModel tárolja a State-et és feldolgozza az Intent-eket, a View feliratkozik a State-re és elküldi az Intent-eket. A klasszikus MVVM Two-Way Binding-gal DataBindingen keresztül a Google csak egyszerű, üzleti logika nélküli képernyőkhöz ajánlja. A Jetpack Compose esetében a fő forgatókönyv az UDF explicit eseményfeldolgozással.

Összehasonlító táblázat:

JellemzőMVVM (klasszikus)MVVM + UDF
AdatfolyamNincs meghatározvaSzigorúan egyirányú
ÁllapotváltozásKözvetlenül a setText()-en keresztülCsak Intent → Reducer útján
Single Source of TruthNemIgen
Reduktor tesztelhetőségeAlacsonyMagas (tiszta függvény)
Google ajánlásaElavult megközelítésElsődleges Compose-hoz

Gyakori hibák az UDF bevezetésekor

A leggyakoribb hiba — mellékhatások a Reducer-en belül. Az MVVM-hez szokott fejlesztők közvetlenül az Intent kezelőjébe helyezik a hálózati kéréseket, ami a Reducer-t tisztátalan függvénnyé teszi és tönkreteszi a tesztelhetőséget. Minden hatást értékként (Effect / SideEffect) kell visszaadni, és a keretrendszer infrastruktúrájának kell végrehajtania. Androidon ehhez korutinokat használnak a ViewModel-ben, TCA-ban — Effect.run-t.

A második hiba — túl részletes Intent-ek. Minden billentyűlenyomás, csúszka mozgatás és szövegváltoztatás külön Intent-et generál. Bemeneti mezőknél ez túlzó — ilyen esetekben elfogadható a Binding használata egyirányú folyammal az űrlapon belül (lokális állapot), a globális Intent-et pedig csak jelentős műveleteknél (küldés, navigáció) elküldeni.

A harmadik hiba — a hatások megszakításának hiánya. Ha a felhasználó elhagyja a képernyőt, és a korutin vagy Task tovább fut, az eredmény egy már megsemmisített View-ra alkalmazható. Androidon használja a viewModelScope.cancel()-t vagy a takeWhileActive()-ot; TCA-ban a hatások automatikusan megszakadnak a Store megsemmisítésekor. A Google Issue Tracker (2024) szerint a befejezetlen korutinokból származó szivárgások a Compose alkalmazások összeomlásainak top-5 okába tartoznak.

Gyakran Ismételt Kérdések

Miben különbözik az UDF az MVI-től?

Az MVI (Model-View-Intent) — az UDF speciális esete három kötelező elemmel: Intent (szándék), Model (állapot), View (megjelenítés). A fő különbség, hogy az MVI-ben a képernyő minden állapotát egyetlen megváltoztathatatlan struktúra (Sealed class) írja le, a View pedig egy tiszta függvény a Model-től a UI-ig. Az UDF egy tágabb kifejezés, amely bármilyen egyirányú folyamot leír, beleértve a Redux-ot és az Elm-et is. A Google dokumentációjában az UDF kifejezés általános névként, az MVI pedig konkrét implementációként szerepel.

Mikor felesleges az UDF?

Az UDF felesleges az egy bemeneti mezővel rendelkező, validálás nélküli képernyőknél, statikus oldalaknál és helykitöltő képernyőknél. Ha a képernyőnek nincs üzleti logikája, és állapota nem függ a felhasználói műveletektől, az UDF felesleges kódot ad hozzá haszon nélkül. Ilyen forgatókönyvekhez elegendő az egyszerű egyirányú kötés vagy a @State a SwiftUI-ban. Az UDF akkor indokolt, ha a képernyő lehetséges állapotainak száma meghaladja a 3–4-et és/vagy mellékhatások jelen vannak.

Hogyan teszteljük az UDF-et?

Mivel a Reducer tiszta függvény, tesztelése a State és Intent különböző kombinációival való meghívásra és az eredményül kapott State és Effect ellenőrzésére redukálódik. Androidon használja a Turbine-t a StateFlow teszteléséhez: küldje el az Intent-et, ellenőrizze a következő State kibocsátást. A TCA-ban van egy beépített TestStore, amely automatikusan ellenőrzi, hogy az Action után csak a várt State mezők változtak-e meg, és csak a várt Effect-ek hajtódtak-e végre.

Kombinálható az UDF és a Two-Way Binding?

Igen, a kombinálás megengedett és gyakran optimális. Az űrlapon belüli bemeneti mezőkhöz használjon lokális Two-Way Binding-ot (vagy Binding-et a SwiftUI-ban), hogy ne hozzon létre Intent-et minden billentyűlenyomáshoz. Az űrlap elküldésekor küldjön egyetlen Intent-et az összegyűjtött adatokkal, amelyet a Reducer dolgoz fel. Ez a hibrid megközelítés — globális UDF lokális Two-Way Binding-gal — a kereskedelmi SwiftUI alkalmazások 70%-ában használatos (Swift Community Survey 2024 adatok).

Mi a közös az UDF-ben, Redux-ban és Elm-ben?

Mindhárom minta egyirányú adatfolyamot valósít meg egyetlen igazságforrással. Az Elm (2012) — egy funkcionális nyelv, amely elsőként vezette be a tiszta Model → View → Update ciklust. A Redux (2015) adaptálta az Elm-et JavaScriptre a Store, Reducer és Action koncepciójával. Az UDF ezen ötletek általánosítása mobilfejlesztésre. Mindhárom megközelítés garantálja a változások kiszámíthatóságát az állapot atomi (oszthatatlan) frissítésein keresztül.

Összefoglalás

  • Unidirectional Data Flow (UDF) — minta egyirányú State → View → Intent → Reducer ciklussal, amely garantálja az állapotváltozások kiszámíthatóságát.
  • Androidon az UDF ViewModel + StateFlow + sealed class Intent útján, iOS-en TCA (Reducer + Store) vagy natív Observable segítségével valósul meg.
  • A Google 2023-tól kezdve az UDF-et ajánlja fő architektúraként a Jetpack Compose számára.
  • Reducer — tiszta függvény mellékhatások nélkül; minden hálózati kérés és adatbázis-művelet az Effect rétegbe kerül.
  • Az UDF megszünteti a Two-Way Binding végtelen ciklusainak problémáját az explicit Intent-feldolgozás és a Single Source of Truth révén.
  • Fő kockázatok — mellékhatások a Reducer-ben, túl részletes Intent-ek és befejezetlen korutinok.
  • A hibrid megközelítés (lokális Two-Way Binding az űrlapban + globális UDF) optimális a legtöbb kereskedelmi alkalmazáshoz.

Kulcsrakész mobilalkalmazást fejlesztünk

Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.

Projekt megbeszélése

Olvassa el is