The core:presentation module implements the presentation layer of the application using MVVM (Model-View-ViewModel) architecture pattern. It contains ViewModels that manage UI state and coordinate business logic through use cases, providing a clean separation between business logic and UI.
- Implement ViewModels for screen state management
- Define UI state models and sealed classes for results
- Handle navigation logic through Router abstraction
- Manage user interactions and coordinate use cases
- Provide reactive data streams for UI consumption
core:presentation (Presentation Layer - Platform-independent ViewModels)
├── presentation/
│ ├── main/ # Notes list screen
│ ├── note/ # Note detail/edit screen
│ ├── splash/ # App startup screen
│ ├── signin/ # Database password entry
│ ├── title/ # Note title editing
│ └── settings/ # App settings
│ └── security/ # Password management
│ ├── enter/ # Enter password
│ ├── confirm/# Confirm password
│ └── change/ # Change password
├── interactor/ # Presentation-facing interactors (Adaptive, Snackbar)
└── navigation/
├── AppNavGraph.kt # Navigation graph definition
└── Router.kt # Navigation abstraction
Each screen has a dedicated ViewModel following MVVM pattern:
MainViewModel: Manages the note list viaSafeRepo.noteDAO, delegates sort updates toNoteSortUseCase, keeps the encrypted database alive, and coordinatesAdaptiveInteractorso adaptive layouts stay in sync.NoteListResult: Sealed interface withLoading,Success(result: Flow<PagingData<Note>>, selectedId: Long?, noteSort: NoteSort), andErrorvariants.PreferencesRepoImpl: MultiplatformPreferencesRepoimplementation backed bymultiplatform-settings(Settings), persistingsortField(columnName) andsortOrder(sql).
NoteViewModel: Manages note editing state and contentSaveViewModel: Handles note saving logicDeleteViewModel: Handles note deletion with confirmationNoteResult: Sealed class for note loading states
SplashViewModel: Initial app state and database initializationSignInViewModel: Database password entrySignInResult: Authentication states
SettingsViewModel: App settings managementSettingsCategoriesViewModel: Category selection and adaptive navigation for settingsSecurityResult: Security settings states- Password Management:
EnterViewModel: Enter new passwordConfirmViewModel: Confirm passwordChangeViewModel: Change existing password
EditTitleViewModel: Note title editingEditTitleResult: Title editing states
AdaptiveInteractor: Shared state holder that bridges adaptive navigation betweenMainViewModel,SettingsCategoriesViewModel, router, and UI panes. It also exposesselectedSettingsCategoryIdStateFlowfor settings selection sync andcheckSaveChangeChannelso the list pane can request a save-change confirmation before switching or creating notes in adaptive layouts.ShareInteractor: Multiplatform abstract class (ShareInteractor+ platform implementations) for outbound note text sharing (AndroidIntent.createChooser, iOSUIActivityViewController, Webnavigator.sharewith copyable snackbar fallback, Desktop copyable snackbar feedback viaSnackbarMessage.Copyable) and holding pending inbound shared text (sharedTextStateFlow) until the database is unlocked andMainViewModelcreates and opens the shared note.SnackbarInteractor: Multiplatform contract used by ViewModels to emit UI messages and copy text to clipboard without depending on Compose APIs.
AppNavGraph: Type-safe navigation destinations using sealed classesRouter: Navigation abstraction that sets controllers, propagates adaptive navigator hooks, and exposes generic navigation methods used across ViewModels.
- MVVM (Model-View-ViewModel): Clear separation of concerns
- Action Interface Pattern: Single
onAction()method for event handling (reduces callback hell) - Unidirectional Data Flow: State flows from ViewModel to UI
- Sealed Interfaces for Actions: Type-safe action representation
- Data Classes for State: Immutable state with helper methods
- Single State Exposure: ViewModels expose
StateFlow<Result>for UI state - Dependency Injection: ViewModels receive dependencies through constructor
- Repository Pattern: ViewModels use repositories, not DAOs directly
- Use Case Pattern: Business logic delegated to domain use cases
- Every ViewModel exposes immutable
StateFlow(for example,MainViewModel.stateFlow). - Internal state is kept in private
MutableStateFlowinstances and updated fromviewModelScopeusingCoroutineDispatchersfor threading. - Long-lived collectors (like adaptive selection synchronization) keep a
Jobreference to avoid leaking multiple collectors.
Result types live next to their ViewModels (*Result.kt) and follow these patterns:
NoteListResulttracks loading/success/error and the currently selected note ID for adaptive layouts.NoteResultrepresents the editor state, including validation errors and password prompts.SecurityResult,EnterResult,ConfirmResult, andChangeResultmodel password flows with explicit one-shot events.
When extending behaviour, prefer adding new sealed interface branches instead of ad-hoc booleans in the state.
This module is Kotlin Multiplatform and supports:
- ✅ Android (androidTarget)
- ✅ iOS (iosArm64, iosSimulatorArm64)
- ✅ Desktop JVM (jvm)
- ✅ Web (wasmJs)
All ViewModels are 100% shared across platforms with no platform-specific code.
core:domain- Domain models and use casesfeature:biometric:domain- BiometricInteractor (used by SignInViewModel and SettingsViewModel)feature:backup:domain- Backup use casesandroidx-lifecycle-viewmodel- ViewModel base class (multiplatform)kotlinx-serialization-json- Serialization supportkotlinx-coroutines- Asynchronous programmingandroidx-paging-common- Pagination supportkermit- Logging
core:test:common- Test utilitieskotlinx-coroutines-test- Coroutine testingturbine- Flow testingmockito(Android) - Mocking frameworkandroidx-arch-core-testing(Android) - LiveData/ViewModel testing
MainViewModelTest,NoteViewModelTest, and the security test suite validate action routing, paging integration, and error handling. UseMainDispatcherRuleand fakes for repositories/interactors to keep tests deterministic.- Password flows (
EnterViewModelTest,ConfirmViewModelTest,ChangeViewModelTest) ensure dialog ViewModels stay in sync with encryption use cases. - When adding a new ViewModel, follow the existing pattern: configure coroutine rules, provide fake
Router/SnackbarInteractor, driveonAction, and assert on the exposedStateFlow.
./gradlew :core:presentation:test # Multiplatform tests
./gradlew :core:presentation:androidHostTest # Android host tests (sources in androidHostTest)ViewModels follow Android Architecture Components lifecycle:
- Creation: Constructed with dependencies via Koin DI
- Active: Manages state while screen is active
- Cleared:
onCleared()called when no longer needed - Cleanup: Coroutines in
viewModelScopeautomatically cancelled
The Router interface (see navigation/Router.kt) keeps ViewModels platform-agnostic:
setController/releaseControllerstore the platform navigation controller.- Generic
navigate,navigateClearingBackStack, andpopBackStackaccept strongly typed destinations (AppNavGraph). - Adaptive helpers (
setAdaptiveNavigator,adaptiveNavigateToDetail,adaptiveNavigateBack) coordinate multi-pane navigation viaAdaptiveInteractor. RouterImplin the UI layer backs these calls with Jetpack Navigation and compose-adaptive primitives.
- Each ViewModel centralizes failures in a
handleErrorhelper that logs with Kermit and emits the properResultstate. - Database failures send the user back to
AppNavGraph.SplashusingRouter.navigateClearingBackStack. - Surface user-facing errors via
SnackbarInteractorrather than duplicating dialog logic.
See CONTRIBUTING.md for general guidelines.
- Use
StateFlowfor UI state, notLiveData - Define state as sealed classes for type safety
- One
StateFlowper screen - No UI logic in ViewModels
- All async operations in
viewModelScope - Use Router for navigation, not direct calls
- Interact with UI toasts/snackbars exclusively through
SnackbarInteractor - Keep adaptive navigation in sync by writing selected IDs to
AdaptiveInteractor
- Define a
sealed interface <Screen>Actionalongside the result/state file. - Expose a single
onAction(action: <Screen>Action)entry point; branch internally to small, private helpers. - Apply the pattern on screens with three or more UI callbacks (Main, Note, Settings, password flows).
- Keep lightweight dialogs simple—
SignInViewModel,SaveViewModel, andDeleteViewModelcall methods directly instead of introducing actions. - Always provide a
disposeOneTimeEvents()(or similar) when the UI needs to clear transient signals after handling them.
Benefits: fewer composable parameters, type-safe events, easier snapshot testing, and centralized navigation/error handling.
- Used by:
core:ui,app:android,app:desktop,app:web,app:ios-kit - Depends on:
core:domain - Test dependencies:
core:test:common