É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) — 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ó.
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.
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.
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.
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.
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.
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 |
|---|---|---|
| Adatfolyam | Nincs meghatározva | Szigorúan egyirányú |
| Állapotváltozás | Közvetlenül a setText()-en keresztül | Csak Intent → Reducer útján |
| Single Source of Truth | Nem | Igen |
| Reduktor tesztelhetősége | Alacsony | Magas (tiszta függvény) |
| Google ajánlása | Elavult megközelítés | Elsődleges Compose-hoz |
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
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.
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.
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.
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).
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
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.
Olvassa el is