docs: massive restructuring of documentation, development guides and agent playbooks
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user