meldestelle/docs/06_Frontend/MVVM_UDF_Pattern.md

120 lines
4.4 KiB
Markdown

---
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<VeranstalterListItem> = emptyList(),
val filtered: List<VeranstalterListItem> = 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<VeranstalterState> = _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<VeranstalterListItem> }
class DefaultVeranstalterRepository : VeranstalterRepository {
override suspend fun list(): List<VeranstalterListItem> = 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.