Verstehen Sie, was unidirektionaler Datenfluss ist — ein einseitiger Datenstrom, ein Architekturmuster, bei dem Daten in einem geschlossenen Kreislauf State → View → Intent → Reducer → State ohne Rückkopplungsschleifen bewegt werden. Im Gegensatz zur bidirektionalen Bindung garantiert UDF, dass Zustandsänderungen nur durch explizite Aktionen (Intent/Event) erfolgen, was den Datenfluss vorhersagbar und nachvollziehbar macht. Laut Google I/O 2024 ist UDF die empfohlene Architektur für Jetpack Compose- und SwiftUI-Anwendungen mit mittlerer und hoher Geschäftslogikkomplexität.
Wichtige Punkte
Unidirektionaler Datenfluss (UDF) ist ein Architekturmuster, bei dem Daten in einem geschlossenen Kreislauf in eine Richtung fließen und Rückkopplungsschleifen zwischen View und Model eliminieren. Im Gegensatz zur bidirektionalen Bindung (Two-Way Binding), bei der eine Änderung in der UI sofort das Modell aktualisiert, erfordert UDF eine explizite Aktion (Intent, Event, Action) für jede Zustandsänderung. Dies macht den Datenfluss vollständig vorhersagbar: Zu jedem Zeitpunkt kann bestimmt werden, welche Aktion zum aktuellen Zustand geführt hat.
Das UDF-Konzept stammt aus Web-Frameworks — Redux (JavaScript, 2015) und Elm (2012) — und wurde für die mobile Entwicklung adaptiert. Laut Google I/O 2024 ist UDF zur empfohlenen Architektur für Jetpack Compose geworden und hat das klassische MVVM mit LiveData abgelöst. Auf iOS wird ein ähnlicher Ansatz in The Composable Architecture (TCA) von Point-Free implementiert, der laut Swift Community Survey (2024) von über 15% der iOS-Entwickler verwendet wird.
Der Hauptvorteil von UDF ist die einzige Quelle der Wahrheit (Single Source of Truth, SSOT): Der gesamte Anwendungszustand wird an einem Ort gespeichert und durch streng definierte Operationen geändert. Dies vereinfacht das Debuggen, Testen und die Fehlerreproduktion, da jede Zustandsänderung protokolliert und durch erneutes Senden derselben Intents reproduziert werden kann.
Der grundlegende UDF-Zyklus besteht aus vier Schritten: State (aktueller Zustand) wird in der View dargestellt; der Benutzer führt eine Aktion aus, die zu einem Intent wird; der Intent wird in einem Reducer (reine Funktion) verarbeitet, der einen neuen State erzeugt; der neue Zustand wird zur Neuzeichnung an die View übergeben. Dieser Zyklus wiederholt sich bei jedem Benutzer- oder Systemereignis.
Jedes Element des Zyklus hat eine strikte Verantwortung: State — ein unveränderliches Objekt, das den Bildschirmzustand zu einem bestimmten Zeitpunkt beschreibt; View — eine Funktion, die den State darstellt; Intent — ein Wert, der die Absicht des Benutzers beschreibt (z.B. LoginIntent.Submit); Reducer — eine reine Funktion ohne Nebenwirkungen, die den aktuellen State und Intent entgegennimmt und einen neuen State zurückgibt. Nebenwirkungen (Netzwerkanfragen, Datenbankoperationen) werden in eine separate Middleware- oder Effect-Schicht ausgelagert.
Laut dem Google Android Architecture-Artikel (2024) ist die Reinheit des Reducers eine Schlüsselanforderung: Enthält ein Reducer einen Netzwerkaufruf oder Datenbankschreibzugriff, wird das Testen und Debuggen des Datenflusses unmöglich. Alle Nebenwirkungen müssen in einer ViewModel-Koroutine oder Swift Task vor dem Aufruf des Reducers ausgeführt werden, und das Ergebnis muss als neuer Intent gesendet werden.
In Android basiert die UDF-Implementierung auf drei Jetpack-Komponenten: ViewModel verwaltet den Lebenszyklus, StateFlow bietet einen reaktiven Zustandsstrom, Intent (sealed class) beschreibt alle möglichen Benutzeraktionen. Die View abonniert StateFlow über collectAsState() in Compose oder observe() im View-System.
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) }
)
}Das Beispiel zeigt den vollständigen UDF-Zyklus in Android: LoginIntent beschreibt alle möglichen Aktionen (E-Mail-Änderung, Passwortänderung, Formularabsendung), LoginState ist der unveränderliche Zustand, LoginViewModel verarbeitet Intents und aktualisiert StateFlow, und der Compose-Bildschirm abonniert den Zustand über collectAsState(). Jede Zustandsänderung ist das Ergebnis der Verarbeitung eines bestimmten Intents, was den Datenfluss vollständig transparent macht.
Auf iOS wird UDF durch The Composable Architecture (TCA) von Point-Free oder das native Observable-Muster mit iOS 17+ implementiert. TCA bietet einen fertigen Zyklus von State + Action + Reducer + Store, wobei Store die einzige Quelle der Wahrheit ist und die View Änderungen über @Observable oder ObservableObject abonniert.
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("Anmelden") { viewStore.send(.submit) }
}
}
}
}loginReducer ist eine reine Funktion: Sie führt keine Netzwerkanfragen direkt aus, sondern gibt einen Effect zurück, der von der TCA-Laufzeit ausgeführt wird. Dies ermöglicht es, den Reducer isoliert zu testen, indem Effekte in Tests ersetzt werden. Die View abonniert Store-Änderungen über WithViewStore und sendet Actions über send(). TCA behandelt die Stornierung von Effekten beim Zerstören des Stores automatisch und verhindert so Speicherlecks.
MVVM und UDF werden oft verwechselt, aber es gibt einen grundlegenden Unterschied zwischen ihnen. MVVM ist ein strukturelles Muster, das Code in drei Schichten (Model, View, ViewModel) aufteilt, aber die Richtung des Datenflusses nicht definiert. UDF ist ein Verhaltensmuster, das beschreibt, wie sich Daten innerhalb dieser Struktur bewegen. In MVVM mit LiveData sind sowohl bidirektionale Bindung als auch unidirektionaler Fluss möglich — UDF fügt MVVM strenge Regeln zur Intent-Verarbeitung hinzu.
Laut der Android Developers-Dokumentation (2024) ist die empfohlene Architektur für Compose UDF innerhalb von MVVM: ViewModel speichert State und verarbeitet Intents, View abonniert State und sendet Intents. Google empfiehlt klassisches MVVM mit Two-Way Binding über DataBinding nur für einfache Bildschirme ohne Geschäftslogik. Für Jetpack Compose ist das primäre Szenario UDF mit expliziter Ereignisbehandlung.
Vergleichstabelle:
| Merkmal | MVVM (klassisch) | MVVM + UDF |
|---|---|---|
| Datenfluss | Nicht definiert | Streng unidirektional |
| Zustandsänderung | Direkt über setText() | Nur über Intent → Reducer |
| Single Source of Truth | Nein | Ja |
| Testbarkeit des Reducers | Niedrig | Hoch (reine Funktion) |
| Google-Empfehlung | Veralteter Ansatz | Primär für Compose |
Der häufigste Fehler sind Nebenwirkungen innerhalb des Reducers. Entwickler, die an MVVM gewöhnt sind, platzieren Netzwerkanfragen direkt im Intent-Handler, was den Reducer unrein macht und die Testbarkeit beeinträchtigt. Alle Effekte sollten als Wert (Effect / SideEffect) zurückgegeben und von der Framework-Infrastruktur ausgeführt werden. In Android werden dazu Koroutinen in ViewModel verwendet, in TCA — Effect.run.
Der zweite Fehler sind zu detaillierte Intents. Jeder Tastendruck, Schiebereglerbewegung und Textänderung erzeugt einen separaten Intent. Für Eingabefelder ist dies übertrieben — in solchen Fällen ist es akzeptabel, Binding mit einem unidirektionalen Fluss innerhalb des Formulars (lokaler Zustand) zu verwenden und einen globalen Intent nur für signifikante Aktionen (Absenden, Navigation) zu senden.
Der dritte Fehler ist fehlende Behandlung von Effektstornierungen. Wenn der Benutzer den Bildschirm verlässt, während eine Koroutine oder Task noch ausgeführt wird, kann das Ergebnis auf eine bereits zerstörte View angewendet werden. In Android verwenden Sie viewModelScope.cancel() oder takeWhileActive(); in TCA werden Effekte beim Zerstören des Stores automatisch storniert. Laut Google Issue Tracker (2024) gehören Lecks durch unvollständige Koroutinen zu den Top-5-Absturzursachen in Compose-Anwendungen.
Häufig gestellte Fragen
MVI (Model-View-Intent) ist ein Spezialfall von UDF mit drei obligatorischen Elementen: Intent (Absicht), Model (Zustand), View (Anzeige). Der Hauptunterschied besteht darin, dass in MVI jeder Bildschirmzustand durch eine einzige unveränderliche Struktur (Sealed class) beschrieben wird und View eine reine Funktion von Model zu UI ist. UDF ist ein breiterer Begriff, der jeden unidirektionalen Fluss beschreibt, einschließlich Redux und Elm. In der Google-Dokumentation wird der Begriff UDF als allgemeiner Name verwendet, während MVI eine spezifische Implementierung ist.
UDF ist übertrieben für Bildschirme mit einem einzigen Eingabefeld ohne Validierung, statischen Seiten und Platzhalterbildschirmen. Wenn ein Bildschirm keine Geschäftslogik hat und sein Zustand nicht von Benutzeraktionen abhängt, fügt UDF unnötigen Code ohne Nutzen hinzu. Für solche Szenarien sind unidirektionale Bindung oder einfaches @State in SwiftUI ausreichend. UDF ist gerechtfertigt, wenn die Anzahl der möglichen Bildschirmzustände 3–4 übersteigt und/oder Nebenwirkungen vorhanden sind.
Da der Reducer eine reine Funktion ist, besteht sein Test darin, ihn mit verschiedenen Kombinationen von State und Intent aufzurufen und den resultierenden State und Effect zu überprüfen. In Android verwenden Sie Turbine zum Testen von StateFlow: Senden Sie einen Intent, überprüfen Sie die nächste State-Emission. In TCA gibt es einen integrierten TestStore, der automatisch überprüft, ob sich nach einer Action nur die erwarteten State-Felder geändert haben und nur die erwarteten Effects ausgeführt wurden.
Ja, die Kombination ist akzeptabel und oft optimal. Für Eingabefelder innerhalb eines Formulars verwenden Sie lokales Two-Way Binding (oder Binding in SwiftUI), um bei jedem Tastendruck einen Intent zu vermeiden. Beim Absenden des Formulars senden Sie einen einzigen Intent mit den gesammelten Daten, der vom Reducer verarbeitet wird. Dieser hybride Ansatz — globales UDF mit lokalem Two-Way Binding — wird in 70% der kommerziellen SwiftUI-Anwendungen verwendet (Daten der Swift Community Survey 2024).
Alle drei Muster implementieren einen unidirektionalen Datenfluss mit einer einzigen Quelle der Wahrheit. Elm (2012) — eine funktionale Sprache — führte erstmals den reinen Model → View → Update-Zyklus ein. Redux (2015) adaptierte Elm für JavaScript mit den Konzepten Store, Reducer und Action. UDF ist eine Verallgemeinerung dieser Ideen für die mobile Entwicklung. Alle drei Ansätze garantieren Vorhersagbarkeit von Änderungen durch atomare Zustandsaktualisierungen.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch