mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory. Co-authored-by: Junie <junie@jetbrains.com>
7.9 KiB
7.9 KiB
Anti-Patterns
Quick-reference table of cross-cutting patterns that hurt MVI Compose Multiplatform codebases. Domain-specific anti-patterns (navigation, networking, paging, DI, etc.) live in their respective reference files — see the "Detailed in" column.
For overengineering patterns (bloated base classes, unnecessary use cases, 4-type MVI), see clean-code.md.
Cross-Cutting Anti-Patterns
| Anti-pattern | Why it is harmful | Better replacement | Detailed in |
|---|---|---|---|
| Business logic inside composables | forks source of truth, hurts testability, reruns during composition | move logic into ViewModel/domain services | architecture.md |
| Giant god-ViewModel | blast radius too large, slow reasoning, hard ownership | one ViewModel per screen or independent flow | architecture.md |
Scattered updateState/sendEffect with no structure |
state transitions hard to trace, mutations across callbacks | disciplined onEvent() as single entry point |
clean-code.md |
| Unstable state models (mutable collections, lambdas in state) | defeats Compose skipping, more recomposition | immutable data classes, immutable collections | performance.md |
Duplicated derived data (total, formattedTotal, hasTotal all stored) |
bugs from drift, harder transitions | keep canonical value + derive via computed property | architecture.md |
| Broad state reads in parent composables | recomposition cascades to all children | slice state, pass only required props to each child | performance.md |
| Mutable state passed deep into tree | hidden writes, unpredictable data flow | explicit props + callbacks | compose-essentials.md |
One-off events stored as consumable state (showSnackbarOnce = true) |
event replay on config change, stale effects | separate Effect via Channel |
architecture.md |
| No-op state emissions (copy state when nothing changed) | wasted recomposition cycles | guard unchanged values before updating | performance.md |
| Full-screen loading wipes existing content | bad UX, layout jumps, lost user trust | keep old content + inline refresh indicator | ui-ux.md |
| ViewModel doing platform work directly (share, analytics, navigation) | breaks testability, platform coupling | emit effects, handle in Route composable | architecture.md |
Animation state in ViewModel for no reason (shakeCount, alpha) |
pollutes business state | local composable animation state | animations.md |
| Display strings stored too early (ViewModel emits pre-baked formatted text) | locale inflexibility, state duplication, harder reuse | keep canonical values until presentation boundary | architecture.md |
| Poor lazy list keys (no key or index-based) | state jumps between rows, broken animations | stable key by domain ID | lists-grids.md |
Too many trivial composables (wrappers around single Text/Spacer) |
fragmentation, harder reading | extract only meaningful boundaries | clean-code.md |
| Platform abstraction too early (interfaces for everything before pain) | unnecessary indirection, poor fit | share business logic first, abstract real platform capabilities only | cross-platform.md |
| Forcing MVI migration on existing codebase | churn without value, team friction | respect existing patterns, introduce MVI for new features only | clean-code.md |
| Inline fully qualified package paths | hurts readability, clutters business logic, hides intent behind package noise | import at file top; use import ... as ... for name clashes |
clean-code.md |
Examples
Business logic inside composables
// BAD — logic in composable; untestable, reruns on every recomposition
@Composable
fun CheckoutScreen(viewModel: CheckoutViewModel) {
val state by viewModel.state.collectAsStateWithLifecycle()
val total = state.items.sumOf { it.price * it.qty } // business logic here
val tax = total * 0.08
Text("Total: $${"$"}total Tax: $${"$"}tax")
}
// GOOD — derive in ViewModel/state, composable only renders
data class CheckoutState(
val items: List<LineItem> = emptyList(),
val total: Double = 0.0,
val tax: Double = 0.0,
)
@Composable
fun CheckoutScreen(state: CheckoutState, onEvent: (CheckoutEvent) -> Unit) {
Text("Total: ${state.total} Tax: ${state.tax}")
}
One-off events as consumable state booleans
// BAD — event replays on config change, race between read and reset
data class UiState(val showSnackbar: Boolean = false)
LaunchedEffect(state.showSnackbar) {
if (state.showSnackbar) {
snackbarHostState.showSnackbar("Saved")
viewModel.onEvent(DismissSnackbar) // consumer must remember to reset
}
}
// GOOD — Channel delivers exactly once, survives config change
sealed interface Effect { data class ShowSnackbar(val msg: String) : Effect }
CollectEffect(viewModel.effects) { effect ->
when (effect) {
is Effect.ShowSnackbar -> snackbarHostState.showSnackbar(effect.msg)
}
}
Domain-Specific Anti-Patterns
These reference files contain their own anti-pattern sections with detailed BAD/GOOD code examples:
| Domain | Reference | What it covers |
|---|---|---|
| Architecture & MVI | architecture.md | event handling, state modeling, effect misuse, domain layer violations |
| Overengineering | clean-code.md | bloated base classes, 4-type MVI, use case wrappers, naming |
| Coroutines & Flow | coroutines-flow.md | GlobalScope, blocking dispatchers, unbound scopes, stateIn misuse |
| Performance | performance.md | recomposition, stability, state shape, read boundaries |
| Compose Essentials | compose-essentials.md | side effects, modifier ordering, CompositionLocal |
| Animations | animations.md | ViewModel animation state, graphicsLayer misuse, over-animating |
| Lists & Grids | lists-grids.md | keys, nested scrolling, contentType |
| UI/UX | ui-ux.md | disappearing content, layout jumps, loading states |
| Navigation (shared) | navigation.md | MVI navigation rules, anti-patterns for both Nav 2 and Nav 3 |
| Paging | paging-mvi-testing.md | PagingData in UiState, key misuse, LoadState handling |
| Networking | networking-ktor-testing.md | MockEngine, DI integration, testing anti-patterns |
| Network Architecture | networking-ktor-architecture.md | plugin composition, error strategy, client lifecycle, result wrapper choice |
| Room Database | room-database.md | entity design, DAO patterns, migrations |
| DataStore | datastore.md | singleton enforcement, blocking reads, corruption |
| DI (Koin) | koin.md | module organization, scoping, ViewModel injection |
| DI (Hilt) | hilt.md | module structure, scoping, testing |
| Image Loading | image-loading.md | cache policy, transformations, placeholder usage |
| Testing | testing.md | missing ViewModel tests, mocking DI, testing internals |
| Cross-Platform | cross-platform.md | expect/actual misuse, premature abstraction |
| iOS Interop | ios-swift-interop.md | naming, nullability, Flow bridging |
| Resources | resources.md | Android R vs CMP Res, qualifier usage |
| Material Design | material-design.md | theme setup, component choice, adaptive layouts |
| Accessibility | accessibility.md | missing semantics, touch targets, contrast |
| Gradle & Build | gradle-build.md | hardcoded versions, buildSrc, convention plugin timing |