- commonMain: ~83 files (domain entities, repository interfaces, use cases, utils, Koin module, Ktor networking, Room KMP database, gitlive Firebase repos, auth)
- androidMain: ~142 files (UI, ViewModels, paging/sync repos, datastore impls, DI)
| Phase | Scope | Status |
|---|---|---|
WeatherPreferences, UserDatastore |
||
AuthRepository + FirebaseAuthRepository in commonMain |
||
SafeApiCall, RepositoryCollections |
||
| Item | Location | Blocker |
|---|---|---|
NotesPreferences interface |
androidMain → commonMain | Sort enums need splitting |
SaveNewCatchUseCase |
androidMain | ViewModel data classes (Uri), paged CatchesRepository |
GetPlaceNameUseCase |
androidMain | Android Geocoder — stays |
UserPreferences |
androidMain | Concrete class, Android Context — stays |
SyncStatusManager, SyncWorker, SyncScheduler |
androidMain | WorkManager — stays Android |
Paging repos (FirebaseCatchesPagedRepository, etc.) |
androidMain | Android Paging 3 — stays |
| 12 ViewModels | androidMain | Various Android deps |
| ~99 UI Composables | androidMain | Full CMP UI migration |
Goal: Move ViewModel logic to shared code using KMP-compatible ViewModel.
This is the next meaningful phase. Phases 6-8 have only minor leftovers that can be addressed incrementally alongside this work. Phases 9-11 are complete.
Complete these small items before or alongside ViewModel migration:
The interface depends on CatchesSortValues / PlacesSortValues enums which have Android deps
(@StringRes, R.string.*, Firestore Query.Direction). Strategy: split the enums.
- Create pure enum in commonMain (just enum values, no Android annotations):
// commonMain: domain/entity/common/SortValues.kt
enum class CatchesSortValues { Default, TimeAsc, TimeDesc, NameAsc, NameDesc, FishDesc }
enum class PlacesSortValues { Default, TimeAsc, TimeDesc, NameAsc, NameDesc, CatchesDesc }- Move
NotesPreferencesinterface to commonMain, importing the new common enums - Keep the Android sort extensions (
sort(),toFirestoreOrder(),stringRes) in androidMain as extension functions on the common enums:
// androidMain: ui/utils/enums/SortExtensions.kt
fun PlacesSortValues.sort(list: List<UserMapMarker>): List<UserMapMarker> { ... }
fun PlacesSortValues.toFirestoreOrder(): Pair<String, Query.Direction> { ... }
@get:StringRes val PlacesSortValues.stringRes: Int get() = when(this) { ... }
// Same for CatchesSortValues- Update
NotesPreferencesImpland UI files to use extensions instead of interface property - Verification:
./gradlew :shared:assembleDebug
BaseViewState<T> is used by most ViewModels. Currently it uses @Immutable from
androidx.compose.runtime which is already in commonMain dependencies (compose.runtime).
- Move
ui/viewstates/BaseViewState.kt→commonMain - Update all ViewModel imports
- Verification:
./gradlew :shared:assembleDebug
Multiple ViewModels call SnackbarManager.showMessage(R.string.*). For commonMain, create
an expect/actual or interface-based abstraction:
// commonMain: ui/SnackbarManager.kt
interface SnackbarEventSender {
fun showMessage(messageKey: String)
}Or keep SnackbarManager in androidMain and let VMs that use it stay in androidMain
(simpler approach — UserCatchViewModel, UserPlaceViewModel, MapViewModel stay).
Recommended: Keep VMs with R.string / SnackbarManager in androidMain for now,
migrate them together with Phase 13 (UI) when R.string gets replaced with CMP resources.
Add androidx.lifecycle:lifecycle-viewmodel KMP artifact to commonMain:
// shared/build.gradle.kts
commonMain.dependencies {
implementation(libs.lifecycle.viewmodel) // KMP since 2.8+
}This provides ViewModel + viewModelScope in commonMain. No expect/actual needed.
These ViewModels depend only on interfaces/use cases already in commonMain.
| ViewModel | Dependencies | Action |
|---|---|---|
StatisticsViewModel |
GetCatchStatisticsUseCase (commonMain) |
Move directly; replace Log.e with println or common logger |
EditProfileViewModel |
UserDatastore (commonMain), UserRepository (commonMain) |
Move directly |
Steps for each:
- Move file from
androidMain→commonMain(same package) - Replace
android.util.Logwithprintln()or a common logging abstraction - Verify imports resolve to commonMain types
- Update Koin module registration if needed
- Verification:
./gradlew :shared:assembleDebug
| ViewModel | Dependencies | Blocker | Resolution |
|---|---|---|---|
UserViewModel |
UserRepository, UserDatastore, OfflineRepository, GetUserCatchesUseCase |
findBestCatch() / findFavoritePlace() from ProfileUtils.kt |
Move the util functions to commonMain (pure Kotlin logic on domain entities) |
MainViewModel |
UserRepository, SyncStatusManager |
SyncStatusManager is in androidMain |
Create SyncStatusProvider interface in commonMain with globalSyncState: StateFlow<SyncState>. Impl stays in androidMain |
Steps for UserViewModel:
- Move
findBestCatch()andfindFavoritePlace()fromui/home/profile/ProfileUtils.kt→ commonMain - Move
UserViewModelto commonMain - Verification:
./gradlew :shared:assembleDebug
Steps for MainViewModel:
- Create interface in commonMain:
// commonMain: domain/repository/SyncStatusProvider.kt
interface SyncStatusProvider {
val globalSyncState: StateFlow<SyncState>
}- Make
SyncStatusManagerimplement this interface - Move
MainViewModelto commonMain, depend onSyncStatusProvider - Update Koin binding
- Verification:
./gradlew :shared:assembleDebug
These ViewModels have deep Android dependencies that make them impractical to move
until Phase 13 (UI migration) addresses R.string, SnackbarManager, Uri, Google Maps, etc.
| ViewModel | Platform deps | Stays until |
|---|---|---|
MapViewModel |
Google Maps LatLng, UserPreferences (concrete Android class), LocationManager, SnackbarManager, R.string |
Phase 13h (Map) |
LoginViewModel |
FirebaseAuth (Google SDK), R.string |
Phase 13i (Login) |
WeatherViewModel |
LocationManager |
Phase 13 |
UserCatchViewModel |
android.net.Uri, R.string, SnackbarManager |
Phase 13e (Catch detail) |
UserPlaceViewModel |
R.string, SnackbarManager, paged CatchesRepository |
Phase 13f (Place detail) |
UserCatchesViewModel |
android.util.Log, paged CatchesRepository |
Phase 13d (Notes) |
UserPlacesViewModel |
android.util.Log, paged MarkersRepositoryPaged |
Phase 13d (Notes) |
NewCatchMasterViewModel |
android.net.Uri, java.util.Date, @Immutable, multiple state classes |
Phase 13g (New catch) |
After moving Tier 1 + 2 ViewModels:
// commonMain: di/CommonUseCasesModule.kt (rename to CommonModule.kt or add new module)
val commonViewModelsModule = module {
viewModel { StatisticsViewModel(get()) }
viewModel { EditProfileViewModel(get(), get()) }
viewModel { UserViewModel(get(), get(), get(), get()) }
viewModel { MainViewModel(get(), get()) }
}// androidMain: di/KoinModules.kt
// Remove migrated VMs from androidMain module, keep Tier 3 VMsNote: viewModel { } DSL requires koin-core 4.x (already in use). For commonMain,
use org.koin.core.module.dsl.viewModel from koin-core.
12-pre-a Move sort enums + NotesPreferences to commonMain (~4 files)
12-pre-b Move BaseViewState to commonMain (~1 file)
12-pre-c Decide SnackbarManager strategy (design decision)
12.1 Move StatisticsViewModel to commonMain (Tier 1 — simplest)
12.2 Move EditProfileViewModel to commonMain (Tier 1)
12.3 Move ProfileUtils + UserViewModel to commonMain (Tier 2)
12.4 Create SyncStatusProvider + move MainViewModel (Tier 2)
12.5 Update Koin modules (DI cleanup)
12.6 Verify full build (./gradlew :androidApp:assembleDebug)
Est. files changed: 12-15 Difficulty: Medium Result: 4 ViewModels in commonMain, 8 remain in androidMain (moved with their screens in Phase 13)
Goal: Migrate UI composables from Android Compose to JetBrains Compose Multiplatform.
This is the largest phase (~85 files). Proceed screen-by-screen.
- Phases 6-12 substantially complete
- Replace
R.stringwith Compose Multiplatform resources (composeResources/) - Replace Lottie with a KMP alternative or
expect/actual
| Step | Screens | Blockers |
|---|---|---|
| 13a | Shared views (Buttons, Cards, DefaultViews, AppBar, Counters) |
R.string → resources |
| 13b | Profile (Profile, EditProfile, ProfileViews) |
Coil → Coil KMP |
| 13c | Settings (SettingsScreen, AboutApp) |
Minimal |
| 13d | Notes/Lists (Notes, UserCatchesScreen, UserPlacesScreen, StatisticsScreen) |
Paging Compose |
| 13e | Catch detail (Catch, CatchScreenDialogs) |
Coil for photos |
| 13f | Place detail (Place, PlaceViews) |
Google Maps embed |
| 13g | New Catch flow (NewCatchMaster, pages, weather icons) |
Photo picker, camera |
| 13h | Map screen (MapScreen, MapViews, NewPlaceDialog, MarkerInfoDialog) |
Google Maps → KMP maps |
| 13i | Login (LoginScreen) |
KMPAuth (already multiplatform) |
| 13j | App shell (FishingNotesApp, Home, navigation) |
Navigation Compose KMP |
| Android | KMP Alternative |
|---|---|
R.string.* |
composeResources/values/strings.xml + stringResource() |
R.drawable.* |
composeResources/drawable/ |
| Lottie Compose | compottie or expect/actual |
| Coil | io.coil-kt.coil3:coil-compose (KMP since Coil 3) |
| Google Maps Compose | expect/actual with platform-specific map |
| Accompanist Permissions | Custom expect/actual permission handling |
| Accompanist Pager | Built-in HorizontalPager (Compose 1.5+) |
| AdMob views | expect/actual, keep ads Android-only |
android.net.Uri photo picker |
expect/actual photo picker |
./gradlew :androidApp:assembleDebug- Full visual regression test on device
| Phase | Scope | Est. files | Status |
|---|---|---|---|
| 13 | UI Composables → CMP | 85+ | Next |
Current focus: Phase 13 — UI layer migration