Skip to content

refactor: KMP best practices — androidx ViewModel, .update{}, navigateTo + docs - #14

Merged
jvsena42 merged 3 commits into
mainfrom
refactor/improve-best-practices
Jun 18, 2026
Merged

refactor: KMP best practices — androidx ViewModel, .update{}, navigateTo + docs#14
jvsena42 merged 3 commits into
mainfrom
refactor/improve-best-practices

Conversation

@jvsena42

Copy link
Copy Markdown
Owner

What & why

Aligns Echo's shared presentation layer and the project docs with current KMP best practices, drawing on the conventions proven in the sibling Bitkit apps (synonymdev/bitkit-android / bitkit-ios AGENTS.md).

Three problems addressed:

  1. Inconsistent state mutation — 6 of 15 ViewModels used _state.value =, 9 used _state.update {}.
  2. Hand-rolled VM lifecycle — every VM built its own CoroutineScope(SupervisorJob() + Dispatchers.Main) + manual onDispose(), despite the build already shipping the JetBrains KMP androidx.lifecycle + koin-compose-viewmodel.
  3. Doc driftArchitecture.md documented SQLDelight, multiplatform-settings, and SKIE as the design; none exist in the build (Pubky-only + in-memory cache). Both docs also had an ecoecho package typo.

Changes (3 atomic commits)

  • docs — new prescriptive Coding conventions section in CLAUDE.md (shared/Android/iOS); reconcile Architecture.md to v1 reality (SQLDelight/SKIE marked not-adopted/future); fix package typo.
  • feat(nav)NavController.navigateTo() that dedups the current destination; all EchoNavigation call sites routed through it.
  • refactor — all 15 shared VMs now extend androidx.lifecycle.ViewModel + viewModelScope, every state write uses _state.update {}, DI binds VMs with Koin's viewModel {} DSL, Android routes resolve via koinViewModel() (dropping the DisposableEffect/onDispose blocks), and the 4 VM tests drive viewModelScope via Dispatchers.setMain(StandardTestDispatcher).

kotlinx.collections.immutable for UiState list fields is documented as a recommended follow-up (not in this PR).

Verification

  • detektAll clean (0 findings)
  • :shared:allTests green — commonTest + iOS simulator + Android unit (incl. the 4 migrated VM tests)
  • :composeApp:assembleDebug builds
  • On-emulator smoke run (debug APK, emulator-5554): onboarding auto-login → Home → Decks → Discover → Profile → Paste import (live preview + navigateTo); every VM resolves via koinViewModel with no DI or lifecycle crashes in logcat.

🤖 Generated with Claude Code

jvsena42 and others added 3 commits June 18, 2026 20:04
Add a prescriptive "Coding conventions" section to CLAUDE.md ported from the
sibling Bitkit AGENTS.md (shared/Android/iOS): androidx ViewModel + viewModelScope,
_state.update{} over .value=, koinViewModel routes, navigateTo, immutable-collection
guidance. Fix the eco -> echo package typo.

Reconcile Architecture.md with the v1 reality: repositories are Pubky-only with an
in-memory session cache (no SQLDelight, no multiplatform-settings), secrets via
SecureSessionStore/KVault, SKIE not yet wired. Mark SQLDelight/SKIE sections as
not-adopted/future rather than current design, and point conventions back to CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce NavController.navigateTo(route, builder) which skips navigation when the
route is already the current destination, guarding against duplicate destinations from
rapid taps or re-emitted navigation effects. Route all EchoNavigation call sites through
it instead of raw navController.navigate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate all 15 shared ViewModels off the hand-rolled CoroutineScope/onDispose()
pattern to the multiplatform androidx.lifecycle.ViewModel + viewModelScope, and
standardize every state write on _state.update{} (removing the remaining _state.value=
sites). viewModelScope cancels in onCleared(), so the manual disposal is gone.

- deps: add androidx-lifecycle-viewmodel and koin-core-viewmodel to commonMain
- DI: bind ViewModels with Koin's viewModel{} DSL in SharedModule
- Android routes: resolve via koinViewModel() and drop the DisposableEffect onDispose
  blocks; navigation lifecycle now owns the VM
- tests: drive viewModelScope via Dispatchers.setMain(StandardTestDispatcher) instead
  of injecting a scope

Verified: detektAll clean, :shared:allTests (commonTest + iOS sim + Android unit) green,
composeApp builds, and an on-emulator smoke run (onboarding -> home -> decks -> discover
-> profile -> paste import) resolves every VM with no DI or lifecycle crashes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jvsena42
jvsena42 merged commit e6e71a9 into main Jun 18, 2026
2 checks passed
@jvsena42
jvsena42 deleted the refactor/improve-best-practices branch June 18, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant