Förstå vad Unidirectional Data Flow är — enkelriktat dataflöde, ett arkitekturmönster där data rör sig i en sluten cykel State → View → Intent → Reducer → State utan återkoppling. Till skillnad från dubbelriktad bindning garanterar UDF att tillståndsändring endast sker genom explicita åtgärder (Intent/Event), vilket gör dataflödet förutsägbart och spårbart. Enligt Google I/O 2024 är UDF den rekommenderade arkitekturen för Jetpack Compose- och SwiftUI-applikationer med medelhög och hög affärslogikkomplexitet.
Huvudpunkter
Unidirectional Data Flow (UDF) — ett arkitekturmönster där data rör sig i en riktning längs en sluten cykel, vilket utesluter återkoppling mellan View och Model. Till skillnad från Two-Way Binding, där en förändring i UI omedelbart uppdaterar modellen, kräver UDF en explicit åtgärd (Intent, Event, Action) för varje tillståndsändring. Detta gör dataflödet helt förutsägbart: när som helst kan man avgöra vilken åtgärd som ledde till det aktuella tillståndet.
Konceptet UDF kommer från webbramverk — Redux (JavaScript, 2015) och Elm (2012) — och har anpassats för mobil utveckling. Enligt Google I/O 2024 har UDF blivit den rekommenderade arkitekturen för Jetpack Compose, och ersätter klassisk MVVM med LiveData. På iOS implementeras ett liknande tillvägagångssätt i The Composable Architecture (TCA) från Point-Free, som används av över 15% av iOS-utvecklare enligt Swift Community-undersökningen (2024).
Den främsta fördelen med UDF — Single Source of Truth (SSOT): hela applikationens tillstånd lagras på en plats och ändras genom strikt definierade operationer. Detta förenklar felsökning, testning och reproduktion av buggar, eftersom varje tillståndsändring loggas och kan reproduceras genom att skicka samma Intent igen.
Den grundläggande UDF-cykeln består av fyra steg: State (aktuellt tillstånd) visas i View; användaren utför en åtgärd som omvandlas till Intent (avsikt); Intent bearbetas i Reducer (en ren funktion), som skapar ett nytt State; det nya tillståndet skickas till View för omritning. Denna cykel upprepas vid varje användar- eller systemhändelse.
Varje element i cykeln har ett strikt ansvar: State — ett oföränderligt (immutable) objekt som beskriver skärmens tillstånd vid en given tidpunkt; View — en funktion som visar State; Intent — ett värde som beskriver användarens avsikt (t.ex. LoginIntent.Submit); Reducer — en ren funktion utan bieffekter, som tar emot aktuellt State och Intent och returnerar ett nytt State. Bieffekter (nätverksförfrågningar, databas) flyttas till ett separat Middleware- eller Effect-lager.
Enligt artikeln Google Android Architecture (2024) är Reducer-renhet ett nyckelkrav: om Reducer innehåller ett nätverksanrop eller databasskrivning blir testning och felsökning av dataflödet omöjligt. Alla bieffekter måste utföras i ViewModel-koroutinen eller Swift Task före anropet av Reducer, och resultatet skickas som en ny Intent.
I Android baseras implementeringen av UDF på tre Jetpack-komponenter: ViewModel hanterar livscykeln, StateFlow ger ett reaktivt flöde av tillstånd, Intent (sealed class) beskriver alla möjliga användaråtgärder. View prenumererar på StateFlow via collectAsState() i Compose eller observe() i View-systemet.
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) }
)
}Exemplet visar den fullständiga UDF-cykeln i Android: LoginIntent beskriver alla möjliga åtgärder (ändra e-post, lösenord, skicka formulär), LoginState — det oföränderliga tillståndet, LoginViewModel bearbetar Intent och uppdaterar StateFlow, och Compose-skärmen prenumererar på state via collectAsState(). Varje tillståndsändring är resultatet av bearbetning av en specifik Intent, vilket gör dataflödet helt transparent.
På iOS implementeras UDF via The Composable Architecture (TCA) från Point-Free eller det inbyggda Observable-mönstret med iOS 17+. TCA tillhandahåller en färdig cykel State + Action + Reducer + Store, där Store är den enda sanningskällan och View prenumererar på ändringar via @Observable eller 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("Logga in") { viewStore.send(.submit) }
}
}
}
}Reducern loginReducer — en ren funktion: utför inte nätverksförfrågningar direkt, utan returnerar en Effect som kommer att utföras av TCA-miljön. Detta gör det möjligt att testa reducern isolerat och ersätta effekter i tester. View prenumererar via WithViewStore på Store-ändringar och skickar Action via send(). TCA hanterar automatiskt avbokning av effekter vid förstörelse av Store, vilket förhindrar minnesläckor.
MVVM och UDF blandas ofta ihop, men det finns en fundamental skillnad mellan dem. MVVM är ett strukturellt mönster som delar upp koden i tre lager (Model, View, ViewModel), men definierar inte riktningen på dataflödet. UDF är ett beteendemönster som beskriver hur data rör sig inom denna struktur. I MVVM med LiveData kan det finnas både dubbelriktad bindning och enkelriktat flöde — UDF lägger till strikta regler för Intent-bearbetning till MVVM.
Enligt Android Developers-dokumentationen (2024) är den rekommenderade arkitekturen för Compose UDF inom MVVM: ViewModel lagrar State och bearbetar Intent, View prenumererar på State och skickar Intent. Klassisk MVVM med Two-Way Binding via DataBinding rekommenderas av Google endast för enkla skärmar utan affärslogik. För Jetpack Compose är huvudscenariot UDF med explicit händelsehantering.
Jämförelsetabell:
| Egenskap | MVVM (klassisk) | MVVM + UDF |
|---|---|---|
| Dataflöde | Inte definierat | Strikt enkelriktat |
| Tillståndsändring | Direkt via setText() | Endast via Intent → Reducer |
| Single Source of Truth | Nej | Ja |
| Testbarhet av redcern | Låg | Hög (ren funktion) |
| Googles rekommendation | Föråldrad metod | Primär för Compose |
Det vanligaste misstaget — bieffekter inuti Reducer. Utvecklare som är vana vid MVVM placerar nätverksförfrågningar direkt i Intent-hanteraren, vilket gör Reducer till en oren funktion och förstör testbarheten. Alla effekter måste returneras som ett värde (Effect / SideEffect) och utföras av ramverkets infrastruktur. I Android används koroutiner i ViewModel, i TCA — Effect.run.
Det andra misstaget — alltför detaljerade Intent. Varje tangenttryckning, reglaget dragning och textändring genererar en separat Intent. För inmatningsfält är detta överdrivet — i sådana fall är det acceptabelt att använda Binding med enkelriktat flöde i formuläret (lokalt tillstånd) och skicka global Intent endast vid betydande åtgärder (skicka, navigering).
Det tredje misstaget — ingen hantering av avbokning av effekter. Om användaren lämnar skärmen och koroutinen eller Task fortsätter att köras, kan resultatet tillämpas på en redan förstörd View. I Android använd viewModelScope.cancel() eller takeWhileActive(); i TCA avbryts effekter automatiskt vid förstörelse av Store. Enligt Google Issue Tracker (2024) är läckor från oavslutade koroutiner bland de fem främsta orsakerna till krascher i Compose-applikationer.
Vanliga frågor
MVI (Model-View-Intent) — ett specialfall av UDF med tre obligatoriska element: Intent (avsikt), Model (tillstånd), View (visning). Huvudskillnaden är att i MVI beskrivs varje skärmtillstånd av en enda oföränderlig struktur (Sealed class), och View är en ren funktion från Model till UI. UDF är en bredare term som beskriver vilket enkelriktat flöde som helst, inklusive Redux och Elm. I Googles dokumentation används termen UDF som ett allmänt namn och MVI som en specifik implementering.
UDF är överflödigt för skärmar med ett inmatningsfält utan validering, statiska sidor och platshållarskärmar. Om skärmen inte har någon affärslogik och dess tillstånd inte beror på användaråtgärder, lägger UDF till onödig kod utan nytta. För sådana scenarier räcker det med enkel enkelriktad bindning eller @State i SwiftUI. UDF är motiverat när antalet möjliga skärmtillstånd överstiger 3–4 eller det finns bieffekter.
Eftersom Reducer är en ren funktion, handlar testning om att anropa med olika kombinationer av State och Intent och kontrollera det resulterande State och Effect. I Android, använd Turbine för att testa StateFlow: skicka Intent, kontrollera nästa State-emission. I TCA finns en inbyggd TestStore som automatiskt kontrollerar att efter Action endast förväntade State-fält ändrades och endast förväntade Effect utfördes.
Ja, kombination är tillåten och ofta optimal. För inmatningsfält i ett formulär, använd lokal Two-Way Binding (eller Binding i SwiftUI) för att inte skapa Intent för varje tangenttryckning. Vid formulärskickning, skicka en enda Intent med insamlade data som bearbetas av Reducer. Denna hybridmetod — global UDF med lokal Two-Way Binding — används i 70% av kommersiella SwiftUI-applikationer (data Swift Community Survey 2024).
Alla tre mönster implementerar enkelriktat dataflöde med en enda sanningskälla. Elm (2012) — ett funktionellt språk som först introducerade den rena cykeln Model → View → Update. Redux (2015) anpassade Elm för JavaScript med konceptet Store, Reducer och Action. UDF är en generalisering av dessa idéer för mobil utveckling. Alla tre tillvägagångssätt garanterar förutsägbarhet av förändringar genom atomära (odelbara) tillståndsuppdateringar.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också