--- type: Reference status: ACTIVE owner: Frontend Expert last_update: 2026-04-03 --- ### MVVM + UDF (Unidirectional Data Flow) — Referenz & Vorlage Ziel: Alle ViewModels folgen einem klaren, einheitlichen Muster. Composables rendern nur `State` und senden `Intent`s. Business-Logik liegt im ViewModel, nicht in den UI-Funktionen. #### Prinzipien - Eine State-Klasse pro Screen/ViewModel (unveränderbar, vollständiger UI-Snapshot). - Eine sealed Intent-Hierarchie pro ViewModel (alle Eingaben fließen darüber ein). - Ein ViewModel, das: - Intents entgegennimmt (`send(intent)`), - State über einen `StateFlow` bereitstellt, - Nebenläufigkeit intern kapselt (CoroutineScope), - Repository-Aufrufe bündelt (keine direkten Store-/API-Aufrufe aus Composables). #### Referenz-Implementierung: Veranstalter Dateien: - `frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterViewModel.kt` - `frontend/features/veranstalter-feature/src/jvmMain/.../DefaultVeranstalterRepository.kt` - `frontend/features/veranstalter-feature/src/jvmMain/.../VeranstalterAuswahlScreen.kt` (verwendet das ViewModel) State + Intent (verkürzt): ```kotlin data class VeranstalterState( val isLoading: Boolean = false, val searchQuery: String = "", val list: List = emptyList(), val filtered: List = emptyList(), val selectedId: Long? = null, val errorMessage: String? = null, ) sealed interface VeranstalterIntent { data object Load : VeranstalterIntent data class SearchChanged(val query: String) : VeranstalterIntent data class Select(val id: Long?) : VeranstalterIntent data object ClearError : VeranstalterIntent } ``` ViewModel (verkürzt): ```kotlin class VeranstalterViewModel(private val repo: VeranstalterRepository) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val _state = MutableStateFlow(VeranstalterState(isLoading = true)) val state: StateFlow = _state init { send(VeranstalterIntent.Load) } fun send(intent: VeranstalterIntent) { when (intent) { is VeranstalterIntent.Load -> load() is VeranstalterIntent.SearchChanged -> reduce { it.copy(searchQuery = intent.query) }.also { filter() } is VeranstalterIntent.Select -> reduce { it.copy(selectedId = intent.id) } is VeranstalterIntent.ClearError -> reduce { it.copy(errorMessage = null) } } } // load(), filter(), reduce() wie in Referenzdatei } ``` Repository-Vertrag und JVM-Adapter (Prototyp, Fake-Store): ```kotlin interface VeranstalterRepository { suspend fun list(): List } class DefaultVeranstalterRepository : VeranstalterRepository { override suspend fun list(): List = FakeVeranstalterStore .all() .map { it.toListItem() } } ``` Composable-Verwendung (verkürzt): ```kotlin @Composable fun VeranstalterAuswahlScreen(onZurueck: () -> Unit, onWeiter: (Long) -> Unit) { val vm = remember { VeranstalterViewModel(DefaultVeranstalterRepository()) } val state by vm.state.collectAsState() OutlinedTextField( value = state.searchQuery, onValueChange = { vm.send(VeranstalterIntent.SearchChanged(it)) }, ) LazyColumn { items(state.filtered) { v -> Row(Modifier.clickable { vm.send(VeranstalterIntent.Select(v.id)) }) { /* ... */ } } } Button(enabled = state.selectedId != null) { state.selectedId?.let { onWeiter(it) } } } ``` #### Regeln (verbindlich) - MVVM + UDF ist Standard. Keine direkten `StoreV2`- oder API-Aufrufe in Composables (auch nicht in `onSaved`-Callbacks usw.). - Kein lokaler `remember`-Zustand für Business-Logik. UI-Interaktionen senden ausschließlich Intents ans ViewModel. - Persistenz/Netzwerk-Zugriffe laufen im Repository. Das ViewModel injiziert das Repository (später per DI). - State ist die Single Source of Truth pro Screen. #### Siehe auch - Weitere Beispiele: `ReiterViewModel`, `PferdeViewModel`, `PingViewModel` in `frontend/features/*/presentation/` - Koin-Integration (VM-Erzeugung in Composables): `org.koin.compose.viewmodel.koinViewModel` #### Vorlage für neue ViewModels 1. `data class UiState(...)` 2. `sealed interface Intent { ... }` 3. `class XxxViewModel(repo: XxxRepository) { fun send(intent) ... }` 4. Composable: `val state by vm.state.collectAsState()` und `vm.send(...)` an Interaktionsstellen. Diese Datei dient als Muster-Dokument für alle zukünftigen Frontend-Features.