-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathllms.txt
More file actions
317 lines (260 loc) · 12.4 KB
/
Copy pathllms.txt
File metadata and controls
317 lines (260 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# MVVMate - LLM Agent Instructions & Context
This file contains the complete context, rules, and best practices for writing code using the `MVVMate` library in a Kotlin/Compose Multiplatform environment. As an AI Agent, adhere STRICTLY to these rules when writing or modifying code in this repository or in projects that use MVVMate.
## 1. Core Concepts
MVVMate is a minimal, type-safe state management library based on the Model-View-Intent (MVI) pattern.
Every screen MUST define a strict contract consisting of:
1. **State (`UiState`)**: A flat `data class` representing everything the UI needs to render.
2. **Action (`UiAction`)**: A `sealed interface` representing user intentions/events.
3. **Effect (`UiEffect`)** (Optional): A `sealed interface` for one-time events like navigation or toasts.
### Rule 1: Always define contracts first before ViewModels
```kotlin
data class ExampleState(
val isLoading: Boolean = false,
val data: String = "",
val error: String? = null
) : UiState
sealed interface ExampleAction : UiAction {
data object LoadData : ExampleAction
data class UpdateInput(val text: String) : ExampleAction
}
sealed interface ExampleEffect : UiEffect {
data class ShowToast(val message: String) : ExampleEffect
}
```
---
## 2. Choosing the Right BaseViewModel
Select the base class based on the screen's requirements.
* `BaseViewModel<S, A>`: Simple state and actions.
* `BaseViewModelWithEffect<S, A, E>`: Needs one-time navigation/toasts.
* `BaseNetworkViewModel<S, A>`: Needs to make API network calls, manages loading state, retry logic, timeouts, and `AppError`.
* `BaseActionsViewModel<S, A>`: Needs to run multiple actions sequentially, in parallel, chained, or batched.
* `BaseNetworkActionsViewModel<S, A>`: Needs both networking tools and multi-action routing.
### Rule 2: State updates must be done using `updateState`
Never mutate state directly. Keep state flat, use `copy()`:
```kotlin
updateState { copy(isLoading = true, error = null) }
```
### Rule 3: Error handling uses `onError` automatically
If `onAction` throws an exception, `onError` is automatically called. Always override `onError` for global safety, especially in `BaseViewModel` or `BaseViewModelWithEffect`. (Note: `CancellationException` is safely ignored).
---
## 3. Network Calls (BaseNetworkViewModel)
When doing API requests, ALWAYS use the built-in `performNetworkCall` or `performNetworkCallWithRetry`.
### Network Loading State Management
You must override these manually in your ViewModel to tell MVVMate how to show global loading indicators:
```kotlin
override fun ExampleState.setGlobalLoadingState() = copy(isLoading = true)
override fun ExampleState.resetGlobalLoadingState() = copy(isLoading = false)
```
*(For partial loading, override `setPartialLoadingState` and `resetPartialLoadingState` which manage a `loadingKeys: Set<String>` in your state).*
### Writing a Network Action
```kotlin
private suspend fun loadData() {
performNetworkCallWithRetry<MyData>(
retries = 3,
isGlobal = true, // uses setGlobalLoadingState automatically
onSuccess = { data ->
updateState { copy(data = data, error = null) }
},
onError = { error -> // Note: error is an AppError instance
updateState { copy(error = error.message) }
},
networkCall = { api.getData() }
)
}
```
---
## 4. Multi-Action Dispatching (BaseActionsViewModel)
If an action has distinct steps, break them into smaller `UiAction` classes and dispatch them.
* `dispatchActionsInSeries(actions)`: Runs one after the other. Wait included.
* `dispatchActionsInParallel(actions)`: Runs all concurrently. Wait included.
* `dispatchBatchActions(actions)`: Fire and forget immediately.
* `dispatchChainedActions(actions, initialData)`: Data pipeline, passing result from `i` to `i+1`.
---
## 5. UI Integration (Compose)
Never pass ViewModels down your compose tree. Consume state and pass lambda callbacks or dispatch actions.
```kotlin
@Composable
fun ExampleScreen(
viewModel: ExampleViewModel = viewModel(),
onNavigateForward: () -> Unit
) {
val state by viewModel.state.collectAsState()
// Handle side effects (navigation, toasts)
LaunchedEffect(Unit) {
viewModel.sideEffects.collect { effect ->
when (effect) {
is ExampleEffect.ShowToast -> { /* show snackbar */ }
}
}
}
// UI Content
if (state.isLoading) CircularProgressIndicator()
Button(onClick = { viewModel.handleAction(ExampleAction.LoadData) }) {
Text("Load")
}
}
```
---
## 6. AI Agent Integration (Agentic UI)
MVVMate lets LLMs drive the UI. If building an Agentic UI component, set up the `AiActionBridge` and `AiActionPolicy`.
1. **Policy**: Strictly define what actions an LLM is *allowed* to take. Block sensitive operations like `DeleteAccount` or `Checkout`.
2. **Bridge**: Provide the parser to convert LLM output into sealed classes, and a redactor to hide PII before sending the state back to the LLM.
---
## 7. Strict Best Practices / DO NOT DOs
* DO NOT write network `try/catch` manually if inheriting `BaseNetworkViewModel`. Let `performNetworkCall` handle it.
* DO NOT expose `MutableStateFlow` or mutators to the Compose UI. UI must call `handleAction(Action)`.
* DO NOT pass `Context` or `NavController` or Android-specific APIs into the ViewModel. Use `SideEffects` instead.
* DO NOT forget to use `withContext(Dispatchers.Default)` for heavy computations instead of blocking the main thread inside `onAction`.
* DO NOT build deeply nested State classes. The State data class should be mostly primitive types or lists. Use `val computed: Int get() = ...` for derived data to avoid inconsistencies.
* DO NOT use `sealed class` for actions/effects, prefer `sealed interface`.
* DO NOT manually map `Throwable` to strings during network errors. `AppError` handles this inside `onError = { error -> ... }` where `error.message` is perfectly readable.
* DO NOT hold state in local variables in Composable screens; always hoist it into `FormField` within the `UiState` via the ViewModel.
---
## 8. ViewModel Testing (`mvvmate-testing`)
Testing in MVVMate uses a Turbine-based DSL. ALWAYS use `runTest` from `kotlinx-coroutines-test`.
### Rule 4: Test states and effects in chronological order
* For simple ViewModels: Use `viewModel.test { ... }`
* For ViewModels with side effects: Use `viewModel.testEffects { ... }`
### ViewModelTestScope API (for `BaseViewModel`)
* `dispatchAction(action)` — dispatch a UiAction under test
* `expectState { predicate }` — await next state, assert predicate returns true
* `expectStateEquals(state)` — await next state, assert strict equality
* `awaitState()` — await and return next state without assertions
* `skipStates(count)` — consume and ignore next `count` states
* `cancelAndIgnoreRemainingStates()` — cancel, discard remaining
### ViewModelEffectTestScope API (for `BaseViewModelWithEffect`)
* `expectState { predicate }` — await, assert next emission is a State
* `expectStateEquals(state)` — await, assert next State equals expected
* `expectEffectEquals(effect)` — await, assert next emission is the exact Effect
* `expectEffectClass<T>()` — await, assert next emission is an Effect of type T
* `awaitEvent()` — await next emission (State or Effect)
* `skipEvents(count)` — skip next `count` emissions
```kotlin
@Test
fun testStateOnly() = runTest {
val viewModel = CounterViewModel()
viewModel.test {
expectStateEquals(CounterState(count = 0))
dispatchAction(CounterAction.Increment)
expectState { it.count == 1 }
}
}
@Test
fun testWithEffects() = runTest {
val viewModel = ExampleViewModel()
viewModel.testEffects {
expectState { !it.isLoading }
dispatchAction(ExampleAction.LoadData)
expectState { it.isLoading }
val effect = expectEffectClass<ExampleEffect.ShowToast>()
expectState { !it.isLoading }
}
}
```
---
## 9. Declarative Form Validation (`mvvmate-forms`)
A type-safe way to manage form state and validation logic entirely within the `UiState`.
### FormField<T>
Fields in your state should be of type `FormField<T>`. Properties: `value`, `errors: List<String>`, `isTouched`, `isDirty`, `isValid`, `isInvalid`.
```kotlin
data class FormState(
val email: FormField<String> = FormField(""),
val age: FormField<String> = FormField("")
) : UiState {
val isFormValid: Boolean get() = email.isValid && age.isValid && email.isDirty && age.isDirty
}
```
### Validation with Validators
Use `Validators` for common rules. Update state using `setValue` to run validation inline.
`setValue(newValue, vararg validators)` — updates value, runs validators, marks dirty & touched.
`markTouched(vararg validators)` — marks touched without changing value, runs validators (for submit).
```kotlin
// Inside ViewModel onAction
is Action.EmailChanged -> updateState {
copy(email = email.setValue(action.email, Validators.required(), Validators.email()))
}
// To show errors only after clicking submit
is Action.Submit -> updateState {
copy(email = email.markTouched(Validators.required(), Validators.email()))
}
```
### Built-in Validators
All return `null` (valid) or error message (invalid). Blank/null values skip validation (except `required`).
* `Validators.required(message)` — not null or blank (default: "Required")
* `Validators.email(message)` — standard email format (default: "Invalid email")
* `Validators.minLength(len, message)` — at least `len` characters
* `Validators.maxLength(len, message)` — at most `len` characters
* `Validators.pattern(regex, message)` — matches regex (default: "Invalid format")
* `Validators.digitsRequired(message)` — strictly digits 0-9 (default: "Must contain only digits")
* `Validators.decimalRequired(message)` — valid decimal number (default: "Must be a valid decimal number")
### Custom Validators
`Validator<T>` is a typealias for `(T) -> String?`. Create custom ones:
```kotlin
fun passwordStrength(): Validator<String?> = { value ->
when {
value == null || value.length < 8 -> "Must be at least 8 characters"
!value.any { it.isUpperCase() } -> "Must contain an uppercase letter"
else -> null
}
}
```
### Compose — Showing Errors
```kotlin
OutlinedTextField(
value = state.email.value,
onValueChange = { viewModel.handleAction(Action.EmailChanged(it)) },
isError = state.email.isTouched && state.email.isInvalid
)
if (state.email.isTouched && state.email.isInvalid) {
Text(state.email.errors.first(), color = MaterialTheme.colorScheme.error)
}
```
---
## 10. Remote Debug (`mvvmate-remote-debug`)
Connects the running app to the MVVMate Studio Plugin via WebSocket for live event streaming and time-travel debugging.
### Setup
```kotlin
val debugLogger = RemoteDebugLogger(
host = "127.0.0.1", port = 8080, path = "/ws/mvvmate"
)
MvvMate.logger = debugLogger
MvvMate.isDebug = true
```
### Rule 5: Override `mapDebugAction` for IDE action injection
To allow the IDE to inject actions, override this in your ViewModel:
```kotlin
override fun mapDebugAction(payload: String): MyAction? {
return when (payload) {
"LoadData" -> MyAction.LoadData
"Refresh" -> MyAction.Refresh
else -> null
}
}
```
### Time-Travel
The `RemoteDebugLogger` stores up to 500 state snapshots. The IDE can send `SET_STATE` commands to restore any snapshot. ViewModels auto-register in `MvvMate.debugBridge` on first action when `isDebug = true`.
### Production Safety
* DO NOT ship `remote-debug` in production builds. Guard behind `BuildConfig.DEBUG`.
* Call `debugLogger.disconnect()` on cleanup.
---
## 11. Timeline Debugging & State Diffing
### TimelineLogger
Records timestamped events into a chronological timeline. Supports delegate chaining.
```kotlin
val timeline = TimelineLogger(delegate = PrintLogger, maxEntries = 500)
MvvMate.logger = timeline
// After reproducing an issue:
timeline.dump() // Print full history
timeline.dumpLast(10) // Print last 10 entries
val report = timeline.formatAsString() // For crash reports
timeline.getEntriesFor("CartViewModel") // Filter by ViewModel
timeline.clear() // Reset
```
### StateDiffUtil
Computes human-readable diffs between state snapshots. Used internally by `PrintLogger` and `TimelineLogger`.
```kotlin
val changes = StateDiffUtil.diff(oldState, newState)
// [FieldChange(field=isLoading, oldValue=false, newValue=true)]
val summary = StateDiffUtil.diffSummary(oldState, newState)
// "isLoading: false → true"
```