Pochopte, co je Unidirectional Data Flow — jednosměrný tok dat, architektonický vzor, při kterém data putují v uzavřeném cyklu State → View → Intent → Reducer → State bez zpětných vazeb. Na rozdíl od obousměrného vázání, UDF zaručuje, že změna stavu nastává pouze prostřednictvím explicitních akcí (Intent/Event), což činí tok dat předvídatelným a sledovatelným. Podle Google I/O 2024 je UDF doporučenou architekturou pro aplikace Jetpack Compose a SwiftUI se střední a vysokou složitostí obchodní logiky.
Hlavní body
Unidirectional Data Flow (UDF) — architektonický vzor, ve kterém se data pohybují jedním směrem podél uzavřeného cyklu, čímž se vylučuje zpětná vazba mezi View a Modelem. Na rozdíl od Two-Way Binding, kde změna v UI okamžitě aktualizuje model, UDF vyžaduje explicitní akci (Intent, Event, Action) pro každou změnu stavu. To činí tok dat zcela předvídatelným: v každém okamžiku lze určit, která akce vedla k aktuálnímu stavu.
Koncepce UDF pochází z webových frameworků — Redux (JavaScript, 2015) a Elm (2012) — a byla adaptována pro mobilní vývoj. Podle Google I/O 2024 se UDF stal doporučenou architekturou pro Jetpack Compose a nahrazuje klasické MVVM s LiveData. Na iOS je podobný přístup implementován v The Composable Architecture (TCA) od Point-Free, kterou používá více než 15 % iOS vývojářů podle průzkumu Swift Community (2024).
Hlavní výhoda UDF — Single Source of Truth (SSOT): celý stav aplikace je uložen na jednom místě a měněn prostřednictvím striktně definovaných operací. To zjednodušuje ladění, testování a reprodukci chyb, protože každá změna stavu je zaznamenána a může být reprodukována opětovným odesláním stejných Intent.
Základní cyklus UDF se skládá ze čtyř kroků: State (aktuální stav) je zobrazen v View; uživatel provede akci, která se přemění na Intent (záměr); Intent je zpracován v Reducer (čistá funkce), který vytvoří nový State; nový stav je předán View k překreslení. Tento cyklus se opakuje při každé uživatelské nebo systémové události.
Každý prvek cyklu má striktní odpovědnost: State — neměnný (immutable) objekt popisující stav obrazovky v daném okamžiku; View — funkce zobrazující State; Intent — hodnota popisující záměr uživatele (např. LoginIntent.Submit); Reducer — čistá funkce bez vedlejších účinků, přijímající aktuální State a Intent a vracející nový State. Vedlejší účinky (síťové požadavky, databáze) jsou přeneseny do samostatné vrstvy Middleware nebo Effect.
Podle článku Google Android Architecture (2024) je čistota Reducer klíčovým požadavkem: pokud Reducer obsahuje síťové volání nebo zápis do databáze, testování a ladění toku dat je nemožné. Všechny vedlejší účinky musí být provedeny v korutině ViewModel nebo Swift Task před voláním Reducer a výsledek odeslán jako nový Intent.
V Androidu je implementace UDF založena na třech komponentách Jetpack: ViewModel spravuje životní cyklus, StateFlow poskytuje reaktivní tok stavů, Intent (sealed class) popisuje všechny možné akce uživatele. View se přihlásí k odběru StateFlow pomocí collectAsState() v Compose nebo observe() v systému View.
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) }
)
}Příklad demonstruje úplný cyklus UDF v Androidu: LoginIntent popisuje všechny možné akce (změna emailu, hesla, odeslání formuláře), LoginState — neměnný stav, LoginViewModel zpracovává Intent a aktualizuje StateFlow, a obrazovka Compose se přihlásí k odběru state pomocí collectAsState(). Každá změna stavu je výsledkem zpracování konkrétního Intent, což činí tok dat zcela transparentním.
Na iOS je UDF implementován prostřednictvím The Composable Architecture (TCA) od Point-Free nebo nativního Observable vzoru od iOS 17+. TCA poskytuje hotový cyklus State + Action + Reducer + Store, kde Store je jediným zdrojem pravdy a View se přihlásí k odběru změn pomocí @Observable nebo ObservableObject.
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("Přihlásit se") { viewStore.send(.submit) }
}
}
}
}Reduktor loginReducer — čistá funkce: neprovádí síťové požadavky přímo, ale vrací Effect, který bude proveden prostředím TCA. To umožňuje testovat reduktor izolovaně a nahrazovat efekty v testech. View se pomocí WithViewStore přihlásí k odběru změn Store a odesílá Action pomocí send(). TCA automaticky spravuje zrušení efektů při zničení Store, čímž předchází únikům paměti.
MVVM a UDF jsou často zaměňovány, ale je mezi nimi zásadní rozdíl. MVVM je strukturální vzor, který rozděluje kód do tří vrstev (Model, View, ViewModel), ale neurčuje směr toku dat. UDF je vzor chování, který popisuje, jak se data pohybují uvnitř této struktury. V MVVM s LiveData může existovat jak obousměrné vázání, tak jednosměrný tok — UDF přidává k MVVM přísná pravidla zpracování Intent.
Podle dokumentace Android Developers (2024) je doporučenou architekturou pro Compose UDF uvnitř MVVM: ViewModel ukládá State a zpracovává Intent, View se přihlásí k odběru State a odesílá Intent. Klasické MVVM s Two-Way Binding přes DataBinding Google doporučuje pouze pro jednoduché obrazovky bez obchodní logiky. Pro Jetpack Compose je hlavním scénářem UDF s explicitním zpracováním událostí.
Srovnávací tabulka:
| Vlastnost | MVVM (klasický) | MVVM + UDF |
|---|---|---|
| Tok dat | Nedefinován | Přísně jednosměrný |
| Změna stavu | Přímo přes setText() | Pouze přes Intent → Reducer |
| Single Source of Truth | Ne | Ano |
| Testovatelnost reduktoru | Nízká | Vysoká (čistá funkce) |
| Doporučení Google | Zastaralý přístup | Hlavní pro Compose |
Nejčastější chyba — vedlejší účinky uvnitř Reducer. Vývojáři zvyklí na MVVM umísťují síťové požadavky přímo do obsluhy Intent, což činí Reducer nečistou funkcí a narušuje testovatelnost. Všechny efekty musí být vráceny jako hodnota (Effect / SideEffect) a provedeny infrastrukturou frameworku. V Androidu se k tomu používají korutiny ve ViewModel, v TCA — Effect.run.
Druhá chyba — příliš podrobné Intent. Každé stisknutí klávesy, pohyb posuvníku a změna textu generuje samostatný Intent. Pro vstupní pole je to nadbytečné — v takových případech je přijatelné použít Binding s jednosměrným tokem uvnitř formuláře (lokální stav) a globální Intent odesílat pouze při významných akcích (odeslání, navigace).
Třetí chyba — absence zpracování zrušení efektů. Pokud uživatel opustí obrazovku a korutina nebo Task pokračuje v provádění, výsledek může být aplikován na již zničené View. V Androidu použijte viewModelScope.cancel() nebo takeWhileActive(); v TCA jsou efekty automaticky zrušeny při zničení Store. Podle Google Issue Tracker (2024) patří úniky z nedokončených korutin mezi top-5 příčin pádů aplikací Compose.
Často kladené otázky
MVI (Model-View-Intent) — zvláštní případ UDF se třemi povinnými prvky: Intent (záměr), Model (stav), View (zobrazení). Hlavní rozdíl je v tom, že v MVI je každý stav obrazovky popsán jednou neměnnou strukturou (Sealed class) a View je čistá funkce od Model k UI. UDF je širší pojem popisující jakýkoli jednosměrný tok, včetně Redux a Elm. V dokumentaci Google se termín UDF používá jako obecný název a MVI jako konkrétní implementace.
UDF je nadbytečný pro obrazovky s jedním vstupním polem bez validace, statické stránky a zástupné obrazovky. Pokud obrazovka nemá obchodní logiku a její stav nezávisí na akcích uživatele, UDF přidává zbytečný kód bez přínosu. Pro takové scénáře stačí jednoduché jednosměrné vázání nebo @State ve SwiftUI. UDF je opodstatněný, když počet možných stavů obrazovky přesahuje 3–4 nebo jsou přítomny vedlejší účinky.
Protože Reducer je čistá funkce, jeho testování spočívá ve volání s různými kombinacemi State a Intent a kontrole výsledného State a Effect. V Androidu použijte Turbine pro testování StateFlow: odešlete Intent, zkontrolujte další emisi State. V TCA je vestavěný TestStore, který automaticky kontroluje, že se po Action změnila pouze očekávaná pole State a byly provedeny pouze očekávané Effect.
Ano, kombinování je povoleno a často optimální. Pro vstupní pole uvnitř formuláře použijte lokální Two-Way Binding (nebo Binding ve SwiftUI), abyste nevytvářeli Intent pro každé stisknutí klávesy. Při odeslání formuláře odešlete jeden Intent se shromážděnými daty, který je zpracován Reducerem. Tento hybridní přístup — globální UDF s lokálním Two-Way Binding — se používá v 70 % komerčních SwiftUI aplikací (data Swift Community Survey 2024).
Všechny tři vzory implementují jednosměrný tok dat s jediným zdrojem pravdy. Elm (2012) — funkcionální jazyk, který jako první představil čistý cyklus Model → View → Update. Redux (2015) adaptoval Elm pro JavaScript s konceptem Store, Reducer a Action. UDF je zobecněním těchto myšlenek pro mobilní vývoj. Všechny tři přístupy zaručují předvídatelnost změn prostřednictvím atomických (nedělitelných) aktualizací stavu.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také