Unidirectional Data Flow — vad är det, UDF i Android och iOS

Författare: IT Sectr Publicerad: 2026-02-20 Lästid: 13 min

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) — arkitekturmönster där tillståndet ändras strikt enligt cykeln: användarinmatning → Intent → Reducer → nytt State → omritning av View.
  • I Android implementeras UDF via ViewModel + StateFlow + Intent-bearbetning; i iOS — via @Observable + Reducer-mönster (Composable Architecture).
  • Google rekommenderar UDF som den primära arkitekturen för Jetpack Compose, från och med dokumentationen 2023.
  • UDF eliminerar problemet med oändliga loopar i Two-Way Binding genom en enda källa till sanning (Single Source of Truth).
  • Den största nackdelen — mer boilerplate-kod jämfört med dubbelriktad bindning (State, Intent, Reducer, Effect).

Vad är Unidirectional Data Flow?

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.

Hur fungerar UDF: cykeln State → View → Intent → Reducer

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.

UDF i Android: ViewModel + StateFlow + 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.

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

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.

UDF i iOS: TCA och Observable-mönster

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.

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("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.

UDF versus MVVM: vad är skillnaden?

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:

EgenskapMVVM (klassisk)MVVM + UDF
DataflödeInte definieratStrikt enkelriktat
TillståndsändringDirekt via setText()Endast via Intent → Reducer
Single Source of TruthNejJa
Testbarhet av redcernLågHög (ren funktion)
Googles rekommendationFöråldrad metodPrimär för Compose

Vanliga misstag vid implementering av UDF

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

Vad skiljer UDF från MVI?

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.

När är UDF överflödigt?

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.

Hur testar man UDF?

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.

Kan UDF och Two-Way Binding kombineras?

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

Vad har UDF, Redux och Elm gemensamt?

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

  • Unidirectional Data Flow (UDF) — mönster med enkelriktad cykel State → View → Intent → Reducer som garanterar förutsägbarhet av tillståndsändringar.
  • I Android implementeras UDF via ViewModel + StateFlow + sealed class Intent, i iOS — via TCA (Reducer + Store) eller inbyggt Observable.
  • Google rekommenderar UDF som primär arkitektur för Jetpack Compose från 2023.
  • Reducer — ren funktion utan bieffekter; alla nätverksförfrågningar och databasoperationer flyttas till Effect-lagret.
  • UDF eliminerar problemet med oändliga loopar i Two-Way Binding genom explicit Intent-bearbetning och Single Source of Truth.
  • Huvudrisker — bieffekter i Reducer, alltför detaljerade Intent och oavslutade koroutiner.
  • Hybridmetod (lokal Two-Way Binding i formulär + global UDF) är optimal för de flesta kommersiella applikationer.

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.

Diskutera projektet

Läs också