Unidirectional Data Flow — wat is het, UDF in Android en iOS

Auteur: IT Sectr Gepubliceerd: 2026-02-20 Leestijd: 13 min

Begrijp wat Unidirectional Data Flow is — unidirectionele gegevensstroom, een architectuurpatroon waarbij gegevens in een gesloten cyclus State → View → Intent → Reducer → State bewegen zonder feedback. In tegenstelling tot bidirectionele binding garandeert UDF dat toestandsverandering alleen plaatsvindt via expliciete acties (Intent/Event), wat de gegevensstroom voorspelbaar en traceerbaar maakt. Volgens Google I/O 2024 is UDF de aanbevolen architectuur voor Jetpack Compose en SwiftUI-applicaties met middelmatige tot hoge complexiteit.

Belangrijkste

  • Unidirectional Data Flow (UDF) — architectuurpatroon waarbij toestand strikt volgens de cyclus verandert: gebruikersinvoer → Intent → Reducer → nieuwe State → hertekenen van View.
  • In Android wordt UDF geïmplementeerd via ViewModel + StateFlow + Intent-verwerking; in iOS via @Observable + Reducer-patroon (Composable Architecture).
  • Google beveelt UDF aan als de primaire architectuur voor Jetpack Compose, vanaf documentatie van 2023.
  • UDF elimineert het probleem van oneindige lussen in Two-Way Binding door één enkele bron van waarheid (Single Source of Truth).
  • Het grootste nadeel — meer boilerplate-code vergeleken met bidirectionele binding (State, Intent, Reducer, Effect).

Wat is Unidirectional Data Flow?

Unidirectional Data Flow (UDF) — een architectuurpatroon waarbij gegevens in één richting langs een gesloten cyclus bewegen, waarbij feedback tussen View en Model wordt uitgesloten. In tegenstelling tot Two-Way Binding, waar een verandering in de UI onmiddellijk het model bijwerkt, vereist UDF een expliciete actie (Intent, Event, Action) voor elke toestandsverandering. Dit maakt de gegevensstroom volledig voorspelbaar: op elk moment kan worden bepaald welke actie tot de huidige toestand heeft geleid.

Het UDF-concept komt uit webframeworks — Redux (JavaScript, 2015) en Elm (2012) — en is aangepast voor mobiele ontwikkeling. Volgens Google I/O 2024 is UDF de aanbevolen architectuur geworden voor Jetpack Compose, ter vervanging van klassieke MVVM met LiveData. Op iOS wordt een vergelijkbare aanpak geïmplementeerd in The Composable Architecture (TCA) van Point-Free, die door meer dan 15% van iOS-ontwikkelaars wordt gebruikt volgens de Swift Community-enquête (2024).

Het belangrijkste voordeel van UDF — Single Source of Truth (SSOT): de volledige applicatiestatus wordt op één plek opgeslagen en gewijzigd via strikt gedefinieerde bewerkingen. Dit vereenvoudigt het debuggen, testen en reproduceren van bugs, omdat elke toestandsverandering wordt geregistreerd en kan worden gereproduceerd door dezelfde Intent opnieuw te verzenden.

Hoe werkt UDF: cyclus State → View → Intent → Reducer

De basis-UDF-cyclus bestaat uit vier stappen: State (huidige toestand) wordt weergegeven in View; de gebruiker voert een actie uit die wordt omgezet in Intent (bedoeling); Intent wordt verwerkt in Reducer (een pure functie), die een nieuwe State creëert; de nieuwe toestand wordt naar View gestuurd voor hertekenen. Deze cyclus herhaalt zich bij elke gebruikers- of systeemgebeurtenis.

Elk element van de cyclus heeft een strikte verantwoordelijkheid: State — een onveranderlijk (immutable) object dat de schermtoestand op een bepaald moment beschrijft; View — een functie die State weergeeft; Intent — een waarde die de bedoeling van de gebruiker beschrijft (bijv. LoginIntent.Submit); Reducer — een pure functie zonder bijwerkingen, die huidige State en Intent ontvangt en een nieuwe State retourneert. Bijwerkingen (netwerkverzoeken, databases) worden verplaatst naar een aparte Middleware- of Effect-laag.

Volgens het artikel Google Android Architecture (2024) is de zuiverheid van Reducer een cruciale vereiste: als Reducer een netwerkoproep of databaseschrijfbewerking bevat, wordt het testen en debuggen van de gegevensstroom onmogelijk. Alle bijwerkingen moeten worden uitgevoerd in de ViewModel-coroutine of Swift Task vóór het aanroepen van Reducer, en het resultaat moet worden verzonden als een nieuwe Intent.

UDF in Android: ViewModel + StateFlow + Intent

In Android is de implementatie van UDF gebaseerd op drie Jetpack-componenten: ViewModel beheert de levenscyclus, StateFlow zorgt voor een reactieve stroom van toestanden, Intent (sealed class) beschrijft alle mogelijke gebruikersacties. View abonneert zich op StateFlow via collectAsState() in Compose of observe() in het View-systeem.

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

Het voorbeeld toont de volledige UDF-cyclus in Android: LoginIntent beschrijft alle mogelijke acties (e-mail wijzigen, wachtwoord wijzigen, formulier verzenden), LoginState — de onveranderlijke toestand, LoginViewModel verwerkt Intent en werkt StateFlow bij, en het Compose-scherm abonneert zich op state via collectAsState(). Elke toestandsverandering is het resultaat van de verwerking van een specifieke Intent, wat de gegevensstroom volledig transparant maakt.

UDF in iOS: TCA en Observable-patroon

Op iOS wordt UDF geïmplementeerd via The Composable Architecture (TCA) van Point-Free of het native Observable-patroon met iOS 17+. TCA biedt een kant-en-klare cyclus State + Action + Reducer + Store, waarbij Store de enige bron van waarheid is en View zich abonneert op wijzigingen via @Observable of 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("Inloggen") { viewStore.send(.submit) }
            }
        }
    }
}

De reduceerder loginReducer — een pure functie: voert geen netwerkverzoeken direct uit, maar retourneert een Effect dat door de TCA-omgeving wordt uitgevoerd. Dit maakt het mogelijk om de reduceerder geïsoleerd te testen, waarbij effecten in tests worden vervangen. View abonneert zich via WithViewStore op wijzigingen in Store en verzendt Action via send(). TCA beheert automatisch het annuleren van effecten bij het vernietigen van Store, waardoor geheugenlekken worden voorkomen.

UDF versus MVVM: wat is het verschil?

MVVM en UDF worden vaak verward, maar er is een fundamenteel verschil tussen beide. MVVM is een structureel patroon dat code verdeelt in drie lagen (Model, View, ViewModel), maar niet de richting van de gegevensstroom bepaalt. UDF is een gedragspatroon dat beschrijft hoe gegevens binnen deze structuur bewegen. In MVVM met LiveData kan zowel bidirectionele binding als unidirectionele stroom voorkomen — UDF voegt strikte regels voor Intent-verwerking toe aan MVVM.

Volgens de Android Developers-documentatie (2024) is de aanbevolen architectuur voor Compose UDF binnen MVVM: ViewModel slaat State op en verwerkt Intent, View abonneert zich op State en verzendt Intent. Klassieke MVVM met Two-Way Binding via DataBinding wordt door Google alleen aanbevolen voor eenvoudige schermen zonder bedrijfslogica. Voor Jetpack Compose is het belangrijkste scenario UDF met expliciete gebeurtenisafhandeling.

Vergelijkingstabel:

KenmerkMVVM (klassiek)MVVM + UDF
GegevensstroomNiet gedefinieerdStrikt unidirectioneel
ToestandsveranderingDirect via setText()Alleen via Intent → Reducer
Single Source of TruthNeeJa
Testbaarheid van reduceerderLaagHoog (pure functie)
Aanbeveling GoogleVerouderde aanpakPrimair voor Compose

Veelgemaakte fouten bij het implementeren van UDF

De meest voorkomende fout — bijwerkingen binnen Reducer. Ontwikkelaars die gewend zijn aan MVVM plaatsen netwerkverzoeken direct in de Intent-handler, waardoor Reducer een onzuivere functie wordt en de testbaarheid wordt verbroken. Alle effecten moeten worden geretourneerd als waarde (Effect / SideEffect) en worden uitgevoerd door de framework-infrastructuur. In Android worden hiervoor coroutines in ViewModel gebruikt, in TCA — Effect.run.

De tweede fout — te gedetailleerde Intent. Elke toetsaanslag, schuifregelaarbeweging en tekstwijziging genereert een aparte Intent. Voor invoervelden is dit overdreven — in dergelijke gevallen is het acceptabel om Binding met unidirectionele stroom binnen het formulier te gebruiken (lokale toestand), en een globale Intent alleen te verzenden bij significante acties (verzenden, navigatie).

De derde fout — geen annuleringsafhandeling van effecten. Als de gebruiker het scherm verlaat en de coroutine of Task blijft uitvoeren, kan het resultaat worden toegepast op een reeds vernietigde View. Gebruik in Android viewModelScope.cancel() of takeWhileActive(); in TCA worden effecten automatisch geannuleerd bij vernietiging van Store. Volgens Google Issue Tracker (2024) behoren lekken van onvoltooide coroutines tot de top-5 oorzaken van crashes in Compose-applicaties.

Veelgestelde vragen

Waarin verschilt UDF van MVI?

MVI (Model-View-Intent) — een speciaal geval van UDF met drie verplichte elementen: Intent (bedoeling), Model (toestand), View (weergave). Het belangrijkste verschil is dat in MVI elke schermtoestand wordt beschreven door één onveranderlijke structuur (Sealed class), en View is een pure functie van Model naar UI. UDF is een bredere term die elke unidirectionele stroom beschrijft, inclusief Redux en Elm. In Google-documentatie wordt de term UDF gebruikt als algemene naam, en MVI als specifieke implementatie.

Wanneer is UDF overbodig?

UDF is overbodig voor schermen met één invoerveld zonder validatie, statische pagina's en placeholder-schermen. Als een scherm geen bedrijfslogica heeft en de toestand niet afhankelijk is van gebruikersacties, voegt UDF onnodige code toe zonder voordeel. Voor dergelijke scenario's is eenvoudige unidirectionele binding of @State in SwiftUI voldoende. UDF is gerechtvaardigd wanneer het aantal mogelijke schermtoestanden 3–4 overschrijdt en/of er bijwerkingen zijn.

Hoe test je UDF?

Aangezien Reducer een pure functie is, komt het testen neer op het aanroepen met verschillende combinaties van State en Intent en het controleren van de resulterende State en Effect. Gebruik in Android Turbine voor het testen van StateFlow: verzend Intent, controleer de volgende State-emissie. In TCA is er een ingebouwde TestStore die automatisch controleert of na Action alleen de verwachte State-velden zijn gewijzigd en alleen de verwachte Effecten zijn uitgevoerd.

Kan UDF worden gecombineerd met Two-Way Binding?

Ja, combineren is toegestaan en vaak optimaal. Gebruik voor invoervelden binnen een formulier lokale Two-Way Binding (of Binding in SwiftUI) om niet voor elke toetsaanslag een Intent te maken. Bij het verzenden van het formulier stuurt u één Intent met de verzamelde gegevens, die door Reducer wordt verwerkt. Deze hybride aanpak — globale UDF met lokale Two-Way Binding — wordt gebruikt in 70% van commerciële SwiftUI-applicaties (gegevens Swift Community Survey 2024).

Wat hebben UDF, Redux en Elm gemeen?

Alle drie patronen implementeren unidirectionele gegevensstroom met één enkele bron van waarheid. Elm (2012) — een functionele taal die als eerste de pure cyclus Model → View → Update introduceerde. Redux (2015) paste Elm aan voor JavaScript met het concept van Store, Reducer en Action. UDF is een generalisatie van deze ideeën voor mobiele ontwikkeling. Alle drie benaderingen garanderen voorspelbaarheid van wijzigingen via atomaire (ondeelbare) toestandsupdates.

Samenvatting

  • Unidirectional Data Flow (UDF) — patroon met unidirectionele cyclus State → View → Intent → Reducer, dat voorspelbaarheid van toestandsveranderingen garandeert.
  • In Android wordt UDF geïmplementeerd via ViewModel + StateFlow + sealed class Intent, in iOS via TCA (Reducer + Store) of native Observable.
  • Google beveelt UDF aan als primaire architectuur voor Jetpack Compose, vanaf 2023.
  • Reducer — pure functie zonder bijwerkingen; alle netwerkverzoeken en databasebewerkingen worden verplaatst naar de Effect-laag.
  • UDF elimineert het probleem van oneindige lussen in Two-Way Binding door expliciete Intent-verwerking en Single Source of Truth.
  • Belangrijkste risico's — bijwerkingen in Reducer, te gedetailleerde Intent en onvoltooide coroutines.
  • Hybride aanpak (lokale Two-Way Binding in formulier + globale UDF) is optimaal voor de meeste commerciële applicaties.

We ontwikkelen een mobiele applicatie turnkey

IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.

Bespreek het project

Lees ook