feat(docs): add comprehensive feature layer instructions and component registry

- Add feature-layer/instructions/ with modular implementation guides:
  - VIEWMODEL.md: MVI patterns (State, Event, Action, Internal Actions)
  - COMPOSE.md: Screen patterns, theming, component hierarchy
  - NAVIGATION.md: Type-safe routes, NavGraph, transitions
  - DI.md: Koin module patterns and registration
  - UPDATING_FEATURE.md: v2.0 UI redesign and improvement workflow

- Add core-layer/COMPONENTS.md with hybrid lookup strategy:
  - Static registry for fast O(1) component lookup
  - Dynamic fallback for discovering new components
  - Auto-update rules to keep registry current
  - Naming conventions (Kpt*, Mifos*, [Feature]*, [Screen]*)

- Update LAYER_GUIDE.md:
  - Add Table of Contents for quick navigation
  - Add "Creating New Feature" step-by-step guide
  - Add component hierarchy and placement rules
  - Add Component Creation cross-update rules
  - Reference modular instruction files
This commit is contained in:
Rajan Maurya 2026-01-05 03:21:41 +05:30
parent c4bcc50352
commit ee27d4aa95
7 changed files with 2984 additions and 218 deletions

View File

@ -0,0 +1,352 @@
# Component Discovery Guide
> **Purpose**: Fast component lookup with automatic updates
> **Pattern**: Static first → Dynamic fallback → Auto-update
---
## Table of Contents
1. [Lookup Strategy](#lookup-strategy)
2. [Static Component Registry](#static-component-registry)
3. [Dynamic Discovery](#dynamic-discovery)
4. [Naming Conventions](#naming-conventions)
5. [Auto-Update Rules](#auto-update-rules)
6. [Component Placement](#component-placement)
---
## Lookup Strategy
```
┌─────────────────────────────────────────────────────────────────┐
│ STEP 1: Check Static Registry (Fast) │
│ → Look in tables below for existing component │
├─────────────────────────────────────────────────────────────────┤
│ STEP 2: If Not Found → Dynamic Search (Fallback) │
│ → Run discovery commands to find in source │
├─────────────────────────────────────────────────────────────────┤
│ STEP 3: If Found Dynamically → Update Static Registry │
│ → Add new component to appropriate table below │
└─────────────────────────────────────────────────────────────────┘
```
**Why This Pattern:**
- Static lookup is instant (read from file)
- Dynamic search catches new components
- Auto-update keeps registry current
---
## Static Component Registry
### Foundation Components (core-base/designsystem)
**Prefix: `Kpt*`**
#### Components (`component/`)
| Component | Purpose | Usage |
|-----------|---------|-------|
| `KptTopAppBar` | Configurable app bar | Standard/Large/Medium variants |
| `KptShimmerLoadingBox` | Skeleton loading | Loading placeholders |
| `KptSnackbarHost` | Snackbar container | Toast messages |
| `KptAnimationSpecs` | Animation specifications | Standard animations |
| `BounceAnimation` | Bounce effect | Button press feedback |
| `SlideTransition` | Slide animation | Screen transitions |
#### Layouts (`layout/`)
| Layout | Purpose | Usage |
|--------|---------|-------|
| `KptGrid` | Responsive grid | Card grids |
| `KptFlowRow` | Horizontal flow | Tag/chip layouts |
| `KptFlowColumn` | Vertical flow | Wrapping columns |
| `KptStack` | Z-axis stacking | Overlays |
| `KptMasonryGrid` | Masonry layout | Pinterest-style |
| `KptResponsiveLayout` | Adaptive layout | Screen size adaptation |
| `KptSidebarLayout` | Sidebar with content | Navigation drawer |
| `KptSplitPane` | Resizable split | Two-panel layout |
| `AdaptiveListDetailPaneScaffold` | List-detail adaptive | Master-detail |
| `AdaptiveNavigableListDetailScaffold` | Navigable list-detail | Navigable master-detail |
| `AdaptiveNavigableSupportingPaneScaffold` | Supporting pane | Three-pane layout |
| `AdaptiveNavigationSuiteScaffold` | Navigation suite | Adaptive navigation |
#### Theme Tokens
| Token | Access | Values |
|-------|--------|--------|
| Spacing | `KptTheme.spacing.*` | `xs`(4dp), `sm`(8dp), `md`(16dp), `lg`(24dp), `xl`(32dp) |
| Shapes | `KptTheme.shapes.*` | `small`, `medium`, `large` |
| Colors | `KptTheme.colorScheme.*` | Material3 color scheme |
---
### Design System Components (core/designsystem)
**Prefix: `Mifos*`**
| Component | Purpose | Usage |
|-----------|---------|-------|
| `MifosScaffold` | Screen scaffold | Top bar, bottom bar, content |
| `MifosTopAppBar` | App bar | Navigation icon, title, actions |
| `MifosTopBar` | Simple title bar | Title only |
| `MifosButton` | Primary button | Main actions |
| `MifosTextField` | Text input | Form fields |
| `MifosPasswordField` | Password input | Visibility toggle |
| `MifosOtpTextField` | OTP input | Verification codes |
| `MifosSearchTextField` | Search input | Search bars |
| `MifosCard` | Card container | Content cards |
| `MifosBottomSheet` | Bottom sheet | Modal content |
| `MifosAlertDialog` | Alert dialog | Confirmations |
| `MifosBasicDialog` | Basic dialog | Simple messages |
| `MifosLoadingDialog` | Loading dialog | Blocking loader |
| `MifosTab` | Tab item | Tab navigation |
| `MifosTabPager` | Tab pager | Swipeable tabs |
| `MifosDropDownMenu` | Dropdown menu | Selection menu |
| `MifosRadioButton` | Radio button | Single selection |
| `MifosNavigation` | Navigation | Nav components |
---
### Business Components (core/ui)
**Prefix: `Mifos*` or descriptive name**
#### Cards
| Component | Purpose | Usage |
|-----------|---------|-------|
| `MifosAccountCard` | Account display | Account list items |
| `MifosDetailsCard` | Detail display | Information cards |
| `MifosDashboardCard` | Dashboard item | Home dashboard |
| `MifosActionCard` | Action card | Clickable actions |
| `MifosItemCard` | Generic item | List items |
| `MifosLabelValueCard` | Key-value | Detail rows |
| `MifosPoweredCard` | Footer card | "Powered by" |
| `MifosTitleSearchCard` | Title + search | Searchable headers |
#### Lists & Items
| Component | Purpose | Usage |
|-----------|---------|-------|
| `BeneficiaryCard` | Beneficiary item | Beneficiary list |
| `BeneficiariesListing` | Beneficiary list | Full list view |
| `TransactionScreenItem` | Transaction item | Transaction list |
| `FaqItemHolder` | FAQ item | Expandable FAQ |
| `AboutUsItemCard` | About item | About section |
| `MonitorListItemWithIcon` | Icon list item | Settings list |
#### States
| Component | Purpose | Usage |
|-----------|---------|-------|
| `MifosErrorComponent` | Error state | Error display |
| `EmptyDataView` | Empty state | No data |
| `NoInternet` | Network error | Offline state |
| `MifosProgressIndicator` | Loading spinner | Inline loading |
| `MifosStatusComponent` | Status badge | Status display |
| `MifosSuccessDialog` | Success dialog | Confirmation |
#### Forms
| Component | Purpose | Usage |
|-----------|---------|-------|
| `MifosDropDownTextField` | Dropdown field | Form dropdowns |
| `MifosOutlineDropDown` | Outlined dropdown | Outlined variant |
| `MifosDropDownPayFromComponent` | Pay from selector | Transfer forms |
| `MifosCheckBox` | Checkbox | Multi-select |
| `MFStepProcess` | Step indicator | Multi-step forms |
| `FilterTopSection` | Filter header | List filters |
#### User/Profile
| Component | Purpose | Usage |
|-----------|---------|-------|
| `MifosUserImage` | User avatar | Profile images |
| `MifosTextUserImage` | Text avatar | Initials avatar |
| `UserProfileField` | Profile field | Profile display |
| `MifosHiddenTextRow` | Hidden text | Sensitive data |
#### Other
| Component | Purpose | Usage |
|-----------|---------|-------|
| `MifosRoundIcon` | Round icon button | FAB-like |
| `MifosLinkText` | Link text | Clickable links |
| `MifosTextButtonWithTopDrawable` | Text button + icon | Icon buttons |
| `MifosMobileIcon` | App icon | Branding |
| `MifosRadioButtonAlertDialog` | Radio dialog | Selection dialog |
| `MifosAlertDialog` | Alert dialog | Confirmations |
| `MifosTexts` | Text styles | Styled text |
---
## Dynamic Discovery
### When to Use
Use dynamic discovery when:
1. Component not found in static registry above
2. Searching for recently added components
3. Unsure if component exists
### Discovery Commands
```bash
# Foundation components (Kpt*)
ls core-base/designsystem/src/commonMain/kotlin/**/component/
ls core-base/designsystem/src/commonMain/kotlin/**/layout/
# Design system components (Mifos* in designsystem)
ls core/designsystem/src/commonMain/kotlin/**/component/
# Business components (core/ui)
ls core/ui/src/commonMain/kotlin/**/component/
```
### Search by Type
```bash
# Find all Button components
grep -r "@Composable" core/ core-base/ | grep -i "button"
# Find all Card components
grep -r "@Composable" core/ core-base/ | grep -i "card"
# Find all Dialog components
grep -r "@Composable" core/ core-base/ | grep -i "dialog"
# Find all TextField/Input components
grep -r "@Composable" core/ core-base/ | grep -iE "(textfield|input|field)"
# Find loading/progress components
grep -r "@Composable" core/ core-base/ | grep -iE "(loading|progress|shimmer)"
# Find error/empty state components
grep -r "@Composable" core/ core-base/ | grep -iE "(error|empty|nodata)"
```
### Claude Glob Patterns
```
core-base/designsystem/**/component/*.kt
core-base/designsystem/**/layout/*.kt
core/designsystem/**/component/*.kt
core/ui/**/component/*.kt
```
---
## Naming Conventions
### Prefix Rules
| Prefix | Location | Purpose |
|--------|----------|---------|
| `Kpt*` | core-base/designsystem | Foundation/Theme |
| `Mifos*` | core/designsystem | UI primitives |
| `Mifos*` | core/ui | Business components |
| `[Feature]*` | feature/[name]/components | Feature-shared |
| `[Screen]*` | feature/[name]/[screen]/components | Screen-specific |
### Component Type by Name Pattern
| Pattern | Type | Look In |
|---------|------|---------|
| `*Button` | Action | core/designsystem |
| `*TextField`, `*Field` | Input | core/designsystem |
| `*Dialog`, `*Sheet` | Modal | core/designsystem |
| `*Card` | Container | core/ui |
| `*Item` | List item | core/ui |
| `*Component`, `*View` | Composite | core/ui |
| `*Indicator` | Feedback | core/ui |
| `*Grid`, `*Row`, `*Column` | Layout | core-base |
| `*Scaffold`, `*Layout` | Structure | core-base |
---
## Auto-Update Rules
### When to Update This File
| Scenario | Action |
|----------|--------|
| Found in static registry | No update needed |
| Found via dynamic search | ADD to static registry |
| Created new component in core/ | ADD to static registry |
| Created feature component | No update (not in registry) |
### How to Update
When you find a component dynamically that's not in the static registry:
1. Identify the correct table (Foundation/Design System/Business)
2. Add a new row with: Component | Purpose | Usage
3. Keep tables alphabetically sorted within categories
**Example:**
```markdown
| `NewMifosComponent` | Brief purpose | When to use |
```
### What NOT to Update
- Feature-specific components (`feature/*/components/`)
- Screen-specific components (`feature/*/[screen]/components/`)
- Temporary or experimental components
---
## Component Placement
### Decision Tree
```
Creating a new component?
├── Is it a theme/layout primitive?
│ └── YES → core-base/designsystem (Kpt*)
├── Is it a UI primitive (Button, TextField)?
│ └── YES → core/designsystem (Mifos*)
├── Used in 2+ features?
│ └── YES → core/ui (Mifos*)
├── Used across screens in same feature?
│ └── YES → feature/[name]/components/
└── Used only in one screen?
└── YES → feature/[name]/[screen]/components/
```
### After Creating in core/
If you create a new component in `core/` or `core-base/`:
1. **ADD it to the static registry above**
2. Follow the naming convention
3. Include Purpose and Usage columns
---
## Quick Reference
### I need a...
| Need | Check Static Table | If Not Found |
|------|-------------------|--------------|
| Button | Design System Components | `grep -i button core/` |
| Card | Business Components → Cards | `grep -i card core/` |
| List item | Business Components → Lists | `grep -i item core/` |
| Loading | Foundation Components | Already exists: `KptShimmerLoadingBox` |
| Error | Business Components → States | Already exists: `MifosErrorComponent` |
| Empty | Business Components → States | Already exists: `EmptyDataView` |
| Dialog | Design System Components | `grep -i dialog core/` |
| Layout | Foundation Layouts | `grep -i layout core-base/` |
---
## Related Files
- Feature Layer Guide: `feature-layer/LAYER_GUIDE.md`
- Compose Patterns: `feature-layer/instructions/COMPOSE.md`
- Design Spec Patterns: `design-spec-layer/_shared/PATTERNS.md`
---
## Changelog
| Date | Change |
|------|--------|
| 2025-01-05 | Created with hybrid static + dynamic approach |

View File

@ -3,11 +3,20 @@
> **Location**: `feature/`
> **Command**: `/feature [Feature]`
## Table of Contents
1. [Overview](#overview)
2. [Creating New Feature](#creating-new-feature)
3. [Directory Structure](#directory-structure)
4. [Component Organization](#component-organization)
5. [Build Commands](#build-commands)
6. [Cross-Update Rules](#cross-update-rules)
7. [Instructions Reference](#instructions-reference)
---
## Overview
The feature layer contains UI modules with ViewModel + Screen (MVI pattern):
The feature layer contains UI modules following MVI (Model-View-Intent) architecture:
```
┌─────────────────────────────────────────────────────────────────────┐
@ -15,236 +24,269 @@ The feature layer contains UI modules with ViewModel + Screen (MVI pattern):
├─────────────────────────────────────────────────────────────────────┤
│ │
│ feature/[name]/ │
│ ├── [Name]ViewModel.kt → MVI (State, Event, Action) │
│ ├── [Name]Screen.kt → Compose UI │
│ ├── navigation/ → Navigation routes │
│ ├── components/ → Feature-specific components │
│ └── di/[Name]Module.kt → Koin registration │
│ ├── [screen]/ → Screen package │
│ │ ├── [Screen]Screen.kt → Compose UI │
│ │ ├── [Screen]ViewModel.kt → MVI (State, Event, Action) │
│ │ ├── [Screen].kt → State/Event/Action definitions │
│ │ └── components/ → Screen-specific components │
│ ├── components/ → Feature-shared components │
│ ├── navigation/ → Navigation routes │
│ └── di/[Name]Module.kt → Koin registration │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Directory Structure
## Creating New Feature
```
feature/
├── home/
│ └── src/commonMain/kotlin/.../feature/home/
│ ├── HomeScreen.kt
│ ├── HomeViewModel.kt
│ ├── navigation/
│ │ └── HomeNavigation.kt
│ ├── components/
│ │ └── BottomSheetContent.kt
│ └── di/
│ └── HomeModule.kt
├── accounts/
├── auth/
├── beneficiary/
├── client-charge/
├── guarantor/
├── loan-account/
├── loan-application/
├── location/
├── notification/
├── onboarding-language/
├── passcode/
├── qr/
├── recent-transaction/
├── savings-account/
├── savings-application/
├── settings/
├── share-account/
├── share-application/
├── status/
├── third-party-transfer/
└── transfer-process/
```
---
## MVI Architecture
```kotlin
// State - UI state (immutable)
@Immutable
data class [Feature]State(
val isLoading: Boolean = false,
val items: List<Item> = emptyList(),
val error: String? = null,
)
// ScreenState - Loading/Success/Error states
sealed interface [Feature]ScreenState {
data object Loading : [Feature]ScreenState
data object Success : [Feature]ScreenState
data class Error(val message: StringResource) : [Feature]ScreenState
}
// Event - One-shot navigation/effects
sealed interface [Feature]Event {
data class NavigateToDetail(val id: Long) : [Feature]Event
data object NavigateBack : [Feature]Event
}
// Action - User interactions
sealed interface [Feature]Action {
data class OnItemClick(val id: Long) : [Feature]Action
data object OnRefresh : [Feature]Action
// Internal actions for handling async results
sealed interface Internal : [Feature]Action {
data class ReceiveData(val dataState: DataState<Data>) : Internal
}
}
// ViewModel
internal class [Feature]ViewModel(
private val repository: [Feature]Repository,
) : BaseViewModel<[Feature]State, [Feature]Event, [Feature]Action>(
initialState = [Feature]State(),
) {
override fun handleAction(action: [Feature]Action) {
when (action) {
is [Feature]Action.OnItemClick ->
sendEvent([Feature]Event.NavigateToDetail(action.id))
is [Feature]Action.Internal.ReceiveData ->
handleDataResult(action.dataState)
}
}
}
```
---
## Screen Pattern
```kotlin
@Composable
internal fun [Feature]Screen(
navigateToDetail: (Long) -> Unit,
navigateBack: () -> Unit,
modifier: Modifier = Modifier,
viewModel: [Feature]ViewModel = koinViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel) { event ->
when (event) {
is [Feature]Event.NavigateToDetail -> navigateToDetail(event.id)
[Feature]Event.NavigateBack -> navigateBack()
}
}
[Feature]ScreenContent(
state = state,
onAction = viewModel::trySendAction,
modifier = modifier,
)
}
@Composable
private fun [Feature]ScreenContent(
state: [Feature]State,
onAction: ([Feature]Action) -> Unit,
modifier: Modifier = Modifier,
) {
// UI implementation
}
```
---
## Navigation Pattern
```kotlin
// feature/[name]/navigation/[Feature]Navigation.kt
const val [FEATURE]_NAVIGATION_ROUTE = "[feature]_route"
const val [FEATURE]_SCREEN_ROUTE = "[feature]_screen"
fun NavController.navigateTo[Feature]Screen() {
navigate([FEATURE]_SCREEN_ROUTE)
}
fun NavGraphBuilder.[feature]NavGraph(
navigateBack: () -> Unit,
navigateToDetail: (Long) -> Unit,
) {
navigation(
startDestination = [FEATURE]_SCREEN_ROUTE,
route = [FEATURE]_NAVIGATION_ROUTE,
) {
composable(route = [FEATURE]_SCREEN_ROUTE) {
[Feature]Screen(
navigateBack = navigateBack,
navigateToDetail = navigateToDetail,
)
}
}
}
```
---
## Implementation Flow
```
/feature [Feature]
├── 1. Create State, ScreenState, Event, Action
├── 2. Create ViewModel
│ └── Inject Repositories from data layer
├── 3. Create Screen
│ ├── Use EventsEffect for navigation events
│ └── Use collectAsStateWithLifecycle for state
├── 4. Create Components (if needed)
├── 5. Create [Feature]Module.kt
│ └── viewModelOf(::[Feature]ViewModel)
├── 6. Create Navigation
│ └── NavGraphBuilder extension functions
└── 7. Register in cmp-navigation
├── Add to KoinModules.kt
└── Add to navigation graph
```
---
## Koin Module Pattern
```kotlin
// feature/[name]/di/[Feature]Module.kt
val [Feature]Module = module {
viewModelOf(::[Feature]ViewModel)
}
```
---
## Build Command
### Step 1: Create Module Directory
```bash
./gradlew :feature:[name]:build
mkdir -p feature/[name]/src/commonMain/kotlin/org/mifos/mobile/feature/[name]
mkdir -p feature/[name]/src/commonMain/composeResources/values
```
### Step 2: Create build.gradle.kts
```kotlin
// feature/[name]/build.gradle.kts
plugins {
alias(libs.plugins.mifos.cmp.feature)
}
android {
namespace = "org.mifos.mobile.feature.[name]"
}
dependencies {
implementation(projects.core.data)
implementation(projects.core.ui)
implementation(projects.core.designsystem)
}
```
### Step 3: Register in settings.gradle.kts
```kotlin
// settings.gradle.kts
include(":feature:[name]")
```
### Step 4: Create Directory Structure
```
feature/[name]/src/commonMain/kotlin/org/mifos/mobile/feature/[name]/
├── [screen]/
│ ├── [Screen]Screen.kt
│ ├── [Screen]ViewModel.kt
│ ├── [Screen].kt
│ └── components/
├── components/
├── navigation/
│ └── [Feature]Navigation.kt
└── di/
└── [Feature]Module.kt
```
### Step 5: Create strings.xml
```xml
<!-- feature/[name]/src/commonMain/composeResources/values/strings.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="feature_[name]_title">Feature Title</string>
</resources>
```
### New Feature Checklist
- [ ] Module directory created
- [ ] build.gradle.kts configured
- [ ] Registered in settings.gradle.kts
- [ ] Screen package created with Screen, ViewModel, State/Event/Action
- [ ] Navigation setup in navigation/
- [ ] DI module created in di/
- [ ] DI module registered in KoinModules.kt
- [ ] strings.xml created for string resources
- [ ] Build passes: `./gradlew :feature:[name]:build`
---
## Directory Structure
### Single-Screen Feature
For features with one main screen:
```
feature/[name]/
├── [Feature]Screen.kt
├── [Feature]ViewModel.kt
├── [Feature].kt # State/Event/Action
├── components/
│ └── [Feature]Component.kt
├── navigation/
│ └── [Feature]Navigation.kt
└── di/
└── [Feature]Module.kt
```
**Example - Notification:**
```
feature/notification/
├── NotificationScreen.kt
├── NotificationViewModel.kt
├── Notification.kt
├── components/
│ └── NotificationItem.kt
├── navigation/
│ └── NotificationNavigation.kt
└── di/
└── NotificationModule.kt
```
### Multi-Screen Feature
For features with multiple screens:
```
feature/[name]/
├── [screen1]/
│ ├── [Screen1]Screen.kt
│ ├── [Screen1]ViewModel.kt
│ ├── [Screen1].kt
│ └── components/
│ └── [Screen1]Header.kt
├── [screen2]/
│ ├── [Screen2]Screen.kt
│ ├── [Screen2]ViewModel.kt
│ ├── [Screen2].kt
│ └── components/
│ └── [Screen2]Form.kt
├── components/ # Shared across screens
│ └── [Feature]SharedComponent.kt
├── navigation/
│ └── [Feature]Navigation.kt
└── di/
└── [Feature]Module.kt
```
**Example - Auth:**
```
feature/auth/
├── login/
│ ├── LoginScreen.kt
│ ├── LoginViewModel.kt
│ ├── Login.kt
│ └── components/
│ ├── LoginHeader.kt
│ └── LoginForm.kt
├── registration/
│ ├── RegistrationScreen.kt
│ ├── RegistrationViewModel.kt
│ ├── Registration.kt
│ └── components/
│ └── RegistrationSteps.kt
├── otpAuthentication/
│ ├── OtpAuthenticationScreen.kt
│ ├── OtpAuthenticationViewModel.kt
│ └── components/
│ └── OtpInput.kt
├── components/ # Shared auth components
│ ├── AuthHeader.kt
│ └── AuthFooter.kt
├── navigation/
│ └── AuthNavigation.kt
└── di/
└── AuthModule.kt
```
---
## Cross-Update Rules (MANDATORY)
## Component Organization
### String Resources - No Hardcoded Strings
### Component Hierarchy
**NEVER use hardcoded strings or `String.format()` in feature code.**
```
┌─────────────────────────────────────────────────────────────┐
│ Screen-Specific: feature/[name]/[screen]/components/ │
│ → Used only in that screen │
├─────────────────────────────────────────────────────────────┤
│ Feature-Shared: feature/[name]/components/ │
│ → Used across screens in same feature │
├─────────────────────────────────────────────────────────────┤
│ App-Wide: core/ui/component/ │
│ → Used in 2+ features │
├─────────────────────────────────────────────────────────────┤
│ Design System: core/designsystem/component/ │
│ → UI primitives (Button, TextField, Card) │
├─────────────────────────────────────────────────────────────┤
│ Foundation: core-base/designsystem/ │
│ → Theme, layouts (KptTheme, KptGrid) │
└─────────────────────────────────────────────────────────────┘
```
All user-facing strings MUST be defined in `composeResources/values/strings.xml` and accessed via `stringResource()`.
### Component Placement Decision
```
Creating a new component?
├── Used only in this screen?
│ └── feature/[name]/[screen]/components/
├── Used across screens in same feature?
│ └── feature/[name]/components/
├── Used in 2+ features?
│ └── core/ui/component/
├── UI primitive (Button, TextField variant)?
│ └── core/designsystem/component/
└── Theme/Layout component?
└── core-base/designsystem/
```
### Examples
| Component | Location | Reason |
|-----------|----------|--------|
| `LoginForm` | `feature/auth/login/components/` | Only in login screen |
| `AuthHeader` | `feature/auth/components/` | Shared across auth screens |
| `MifosAccountCard` | `core/ui/component/` | Used in accounts, home |
| `MifosButton` | `core/designsystem/component/` | UI primitive |
| `KptTheme` | `core-base/designsystem/` | Foundation |
---
## Build Commands
```bash
# Build specific feature
./gradlew :feature:[name]:build
# Build all features
./gradlew build
# Lint check
./gradlew :feature:[name]:detekt
# Format code
./gradlew spotlessApply
# Run tests
./gradlew :feature:[name]:test
```
---
## Cross-Update Rules
### String Resources
**NEVER use hardcoded strings.** All user-facing strings MUST be in `strings.xml`:
```kotlin
// WRONG - Hardcoded string
@ -256,11 +298,11 @@ Text(text = String.format("Hello, %s!", userName))
// CORRECT - Use stringResource
Text(text = stringResource(Res.string.welcome_message))
// CORRECT - Use stringResource with arguments
// CORRECT - With arguments
Text(text = stringResource(Res.string.hello_user, userName))
```
**strings.xml example:**
**strings.xml:**
```xml
<!-- feature/[name]/src/commonMain/composeResources/values/strings.xml -->
<resources>
@ -269,10 +311,112 @@ Text(text = stringResource(Res.string.hello_user, userName))
</resources>
```
### Status Updates
After implementing a feature, update:
1. `feature-layer/LAYER_STATUS.md` - Feature layer status
2. `design-spec-layer/features/[feature]/STATUS.md` - Feature design status
### Component Creation
**ALWAYS check existing components before creating new ones.**
See [core-layer/COMPONENTS.md](../core-layer/COMPONENTS.md) for complete registry.
**Lookup Strategy:**
```
Step 1: Check Static Registry (Fast)
→ Read core-layer/COMPONENTS.md tables
Step 2: If Not Found → Dynamic Search
→ grep -r "@Composable" core/ | grep -i "[type]"
Step 3: If Found Dynamically → Update Registry
→ Add to core-layer/COMPONENTS.md static tables
```
**Naming Convention:**
| Location | Prefix | Example |
|----------|--------|---------|
| core-base/designsystem | `Kpt*` | `KptGrid`, `KptShimmerLoadingBox` |
| core/designsystem | `Mifos*` | `MifosButton`, `MifosTextField` |
| core/ui | `Mifos*` | `MifosAccountCard`, `MifosErrorComponent` |
| feature/[name]/components | `[Feature]*` | `AuthHeader` |
| feature/[name]/[screen]/components | `[Screen]*` | `LoginForm` |
**Update Rules:**
| Scenario | Action |
|----------|--------|
| Found in static registry | No update needed |
| Found via dynamic search | ADD to static registry |
| Created new component in core/ | ADD to static registry |
| Created feature component | No update needed |
---
## Instructions Reference
For detailed implementation patterns, see:
| Pattern | File | When to Use |
|---------|------|-------------|
| **ViewModel** | [instructions/VIEWMODEL.md](instructions/VIEWMODEL.md) | Creating/updating ViewModel, State, Event, Action |
| **Compose Screen** | [instructions/COMPOSE.md](instructions/COMPOSE.md) | Creating screens, components, UI patterns |
| **Navigation** | [instructions/NAVIGATION.md](instructions/NAVIGATION.md) | Setting up routes, NavGraph |
| **Dependency Injection** | [instructions/DI.md](instructions/DI.md) | Koin module registration |
| **Updating Feature** | [instructions/UPDATING_FEATURE.md](instructions/UPDATING_FEATURE.md) | v2.0 UI redesign, improving existing features |
### Quick Pattern Reference
**ViewModel Pattern:**
```kotlin
internal class [Feature]ViewModel(
private val repository: [Feature]Repository,
) : BaseViewModel<[Feature]State, [Feature]Event, [Feature]Action>(
initialState = [Feature]State()
) {
override fun handleAction(action: [Feature]Action) { ... }
}
```
**Screen Pattern:**
```kotlin
@Composable
internal fun [Feature]Screen(
onNavigateBack: () -> Unit,
viewModel: [Feature]ViewModel = koinViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel.eventFlow) { event -> ... }
[Feature]Content(state = state, onAction = viewModel::trySendAction)
}
```
**Navigation Pattern:**
```kotlin
@Serializable
data object [Feature]Route
fun NavGraphBuilder.[feature]Screen(onNavigateBack: () -> Unit) {
composableWithStayTransitions<[Feature]Route> {
[Feature]Screen(onNavigateBack = onNavigateBack)
}
}
```
**DI Pattern:**
```kotlin
val [Feature]Module = module {
viewModelOf(::[Feature]ViewModel)
}
```
---
## Related Files
- UI Spec: `claude-product-cycle/design-spec-layer/features/[feature]/SPEC.md`
- Design Specs: `claude-product-cycle/design-spec-layer/features/[feature]/SPEC.md`
- Mockups: `claude-product-cycle/design-spec-layer/features/[feature]/MOCKUP.md`
- Patterns: `claude-product-cycle/design-spec-layer/_shared/PATTERNS.md`
- Navigation: `cmp-navigation/src/commonMain/kotlin/cmp/navigation/`
- Feature Status: `claude-product-cycle/feature-layer/LAYER_STATUS.md`

View File

@ -0,0 +1,746 @@
# Compose Screen Patterns
## Table of Contents
1. [Component Hierarchy](#component-hierarchy)
2. [Available Components](#available-components)
3. [Screen Structure](#screen-structure)
4. [Container + Content Pattern](#container--content-pattern)
5. [Section-Based Design](#section-based-design)
6. [Component Placement Rules](#component-placement-rules)
7. [Reusability Rules](#reusability-rules)
8. [Loading States](#loading-states)
9. [Error States](#error-states)
10. [Empty States](#empty-states)
11. [Dialog Management](#dialog-management)
12. [Theming Guidelines](#theming-guidelines)
13. [Preview Patterns](#preview-patterns)
---
## Component Hierarchy
```
┌─────────────────────────────────────────────────────────────┐
│ LEVEL 5: Foundation (core-base/designsystem) │
│ KptTheme, KptTopAppBar, KptShimmerLoadingBox, KptGrid │
├─────────────────────────────────────────────────────────────┤
│ LEVEL 4: Design System (core/designsystem) │
│ MifosButton, MifosTextField, MifosCard, MifosScaffold │
├─────────────────────────────────────────────────────────────┤
│ LEVEL 3: App-Wide (core/ui) │
│ MifosAccountCard, TransactionScreenItem, EmptyDataView │
├─────────────────────────────────────────────────────────────┤
│ LEVEL 2: Feature-Shared (feature/[name]/components/) │
│ AuthHeader, TransferAmountInput │
├─────────────────────────────────────────────────────────────┤
│ LEVEL 1: Screen-Specific (feature/[name]/[screen]/comp/) │
│ LoginForm, AccountDetailsHeader │
└─────────────────────────────────────────────────────────────┘
```
**Rule**: Always check higher levels before creating new components.
---
## Available Components
> **Lookup Strategy**: Static registry first → Dynamic search if not found → Auto-update registry
> See [core-layer/COMPONENTS.md](../../core-layer/COMPONENTS.md) for complete component registry.
### Component Lookup (ALWAYS DO FIRST)
**Step 1: Check Static Registry (Fast)**
```
Read: core-layer/COMPONENTS.md → Static Component Registry section
```
**Step 2: If Not Found → Dynamic Search**
```bash
# Search by component type
grep -r "@Composable" core/ core-base/ | grep -i "[type]"
# Or list directories
ls core-base/designsystem/**/component/
ls core/designsystem/**/component/
ls core/ui/**/component/
```
**Step 3: If Found Dynamically → Update Registry**
```
Add to: core-layer/COMPONENTS.md → appropriate static table
```
### Source Directories
| Layer | Path | Prefix | Purpose |
|-------|------|--------|---------|
| Foundation | `core-base/designsystem/.../component/` | `Kpt*` | Theme, loading, animations |
| Foundation | `core-base/designsystem/.../layout/` | `Kpt*` | Grid, flow, responsive layouts |
| Design System | `core/designsystem/.../component/` | `Mifos*` | UI primitives (button, textfield) |
| Business | `core/ui/.../component/` | `Mifos*` | App-wide (cards, lists, states) |
### Naming Convention Rules
| Prefix | Location | When to Use |
|--------|----------|-------------|
| `Kpt*` | core-base/designsystem | Theme, layout, animation components |
| `Mifos*` | core/designsystem | UI primitives (Button, TextField, Dialog) |
| `Mifos*` | core/ui | Business components (Card, Item, State) |
| `[Feature]*` | feature/[name]/components | Feature-shared components |
| `[Screen]*` | feature/[name]/[screen]/components | Screen-specific components |
### Component Type by Name Pattern
| Pattern | Type | Look In |
|---------|------|---------|
| `*Button` | Action | `core/designsystem` |
| `*TextField`, `*Field` | Input | `core/designsystem` |
| `*Dialog`, `*Sheet` | Modal | `core/designsystem` |
| `*Card` | Container | `core/ui` |
| `*Item` | List item | `core/ui` |
| `*Component`, `*View` | Composite | `core/ui` |
| `*Grid`, `*Row`, `*Column` | Layout | `core-base/designsystem` |
| `*Scaffold`, `*Layout` | Structure | `core-base/designsystem` |
### Theme Tokens (Always Available)
```kotlin
// Spacing
KptTheme.spacing.xs // 4.dp
KptTheme.spacing.sm // 8.dp
KptTheme.spacing.md // 16.dp
KptTheme.spacing.lg // 24.dp
KptTheme.spacing.xl // 32.dp
// Shapes
KptTheme.shapes.small // 4.dp rounded
KptTheme.shapes.medium // 8.dp rounded
KptTheme.shapes.large // 16.dp rounded
// Colors
KptTheme.colorScheme.primary
KptTheme.colorScheme.onPrimary
KptTheme.colorScheme.surface
KptTheme.colorScheme.onSurface
KptTheme.colorScheme.error
```
---
## Screen Structure
### Directory Layout
```
feature/[name]/
├── [screen]/
│ ├── [Screen]Screen.kt # Container composable
│ ├── [Screen]ViewModel.kt # ViewModel
│ ├── [Screen].kt # State/Event/Action
│ └── components/ # Screen-specific components
│ ├── [Screen]Header.kt
│ ├── [Screen]Content.kt
│ └── [Screen]Card.kt
├── components/ # Feature-shared components
│ └── Shared[Feature]Component.kt
├── navigation/
└── di/
```
### File Naming
| File | Naming |
|------|--------|
| Screen | `[Screen]Screen.kt` |
| ViewModel | `[Screen]ViewModel.kt` |
| State/Event/Action | `[Screen].kt` |
| Components | `[Screen][Purpose].kt` |
---
## Container + Content Pattern
### Container Composable
Handles ViewModel integration, events, and dialogs:
```kotlin
@Composable
internal fun [Screen]Screen(
onNavigateBack: () -> Unit,
onNavigateToDetail: (Long) -> Unit,
modifier: Modifier = Modifier,
viewModel: [Screen]ViewModel = koinViewModel(),
) {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
// Handle one-time events
EventsEffect(viewModel.eventFlow) { event ->
when (event) {
is [Screen]Event.NavigateBack -> onNavigateBack()
is [Screen]Event.NavigateToDetail -> onNavigateToDetail(event.id)
is [Screen]Event.ShowToast -> {
scope.launch { snackbarHostState.showSnackbar(event.message) }
}
}
}
// Render dialogs
[Screen]Dialogs(
dialogState = state.dialogState,
onDismiss = remember(viewModel) {
{ viewModel.trySendAction([Screen]Action.OnDismissDialog) }
},
)
// Render content
[Screen]Content(
state = state,
snackbarHostState = snackbarHostState,
onAction = remember(viewModel) { { viewModel.trySendAction(it) } },
modifier = modifier,
)
}
```
### Content Composable
Pure UI, testable:
```kotlin
@Composable
private fun [Screen]Content(
state: [Screen]State,
snackbarHostState: SnackbarHostState,
onAction: ([Screen]Action) -> Unit,
modifier: Modifier = Modifier,
) {
MifosScaffold(
topBar = {
MifosTopAppBar(
title = stringResource(Res.string.screen_title),
onNavigationClick = { onAction([Screen]Action.OnNavigateBack) },
)
},
snackbarHost = { KptSnackbarHost(snackbarHostState) },
modifier = modifier,
) { paddingValues ->
when (state.uiState) {
is [Screen]ScreenState.Loading -> {
[Screen]LoadingContent(
modifier = Modifier.padding(paddingValues)
)
}
is [Screen]ScreenState.Success -> {
[Screen]SuccessContent(
state = state,
onAction = onAction,
modifier = Modifier.padding(paddingValues),
)
}
is [Screen]ScreenState.Error -> {
MifosErrorComponent(
message = stringResource(state.uiState.message),
onRetry = { onAction([Screen]Action.OnRetry) },
modifier = Modifier.padding(paddingValues),
)
}
is [Screen]ScreenState.Empty -> {
EmptyDataView(
modifier = Modifier.padding(paddingValues),
)
}
is [Screen]ScreenState.Network -> {
NoInternet(
onRetry = { onAction([Screen]Action.OnRetry) },
modifier = Modifier.padding(paddingValues),
)
}
}
}
}
```
### Key Patterns
| Pattern | Purpose |
|---------|---------|
| `remember(viewModel)` | Memoize callbacks to prevent recomposition |
| `EventsEffect` | Lifecycle-aware event handling |
| `collectAsStateWithLifecycle` | Lifecycle-aware state collection |
| Separate dialogs | Keep dialog logic isolated |
---
## Section-Based Design
### Rule: Break Screens into Sections
**Bad** - Monolithic screen:
```kotlin
@Composable
private fun AccountContent(state: AccountState) {
Column {
// 200+ lines of mixed UI code
}
}
```
**Good** - Section-based:
```kotlin
@Composable
private fun [Screen]SuccessContent(
state: [Screen]State,
onAction: ([Screen]Action) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier.fillMaxSize()) {
// Header Section
item {
[Screen]HeaderSection(
title = state.title,
subtitle = state.subtitle,
)
}
// Summary Section
item {
[Screen]SummarySection(
balance = state.balance,
currency = state.currency,
)
}
// Actions Section
item {
[Screen]ActionsSection(
onTransfer = { onAction([Screen]Action.OnTransfer) },
onWithdraw = { onAction([Screen]Action.OnWithdraw) },
)
}
// List Header
item {
SectionHeader(title = stringResource(Res.string.transactions))
}
// List Items
items(
items = state.transactions,
key = { it.id }
) { transaction ->
TransactionScreenItem(
transaction = transaction,
onClick = { onAction([Screen]Action.OnTransactionClick(it.id)) },
)
}
}
}
```
### Section Component Template
```kotlin
// feature/[name]/[screen]/components/[Screen]HeaderSection.kt
@Composable
internal fun [Screen]HeaderSection(
title: String,
subtitle: String,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(KptTheme.spacing.md),
) {
Text(
text = title,
style = MifosTypography.titleLarge,
color = KptTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(KptTheme.spacing.xs))
Text(
text = subtitle,
style = MifosTypography.bodyMedium,
color = KptTheme.colorScheme.onSurfaceVariant,
)
}
}
```
---
## Component Placement Rules
### Decision Tree
```
Creating a new component?
├── Is it a UI primitive (Button variant, TextField variant)?
│ └── YES → core/designsystem/component/
├── Used in 2+ features?
│ └── YES → core/ui/component/
├── Used across screens in same feature?
│ └── YES → feature/[name]/components/
└── Used only in this screen?
└── YES → feature/[name]/[screen]/components/
```
### Examples
| Component | Location | Reason |
|-----------|----------|--------|
| `MifosOutlinedButton` | `core/designsystem/` | UI primitive |
| `TransactionScreenItem` | `core/ui/` | Used in accounts, home, recent |
| `AuthHeader` | `feature/auth/components/` | Used in login, register, otp |
| `LoginForm` | `feature/auth/login/components/` | Only in login screen |
---
## Reusability Rules
### When to Move to core/ui
Move a component when:
1. **Used in 2+ features**
2. **Represents a business concept** (Account, Transaction, Beneficiary)
3. **Has consistent behavior** across uses
```kotlin
// Step 1: Identify repeated component
// feature/accounts/components/AccountCard.kt
// feature/home/components/AccountCard.kt <- Duplication!
// Step 2: Move to core/ui
// core/ui/component/MifosAccountCard.kt
// Step 3: Update imports everywhere
import org.mifos.mobile.core.ui.component.MifosAccountCard
```
### When to Move to Feature components/
Move from screen-specific when:
1. Used in 2+ screens within same feature
2. Shared styling/behavior within feature
---
## Loading States
### Shimmer Loading
```kotlin
@Composable
private fun [Screen]LoadingContent(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(KptTheme.spacing.md),
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
) {
// Header skeleton
KptShimmerLoadingBox(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.clip(KptTheme.shapes.medium),
)
// Card skeletons
repeat(3) {
KptShimmerLoadingBox(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.clip(KptTheme.shapes.medium),
)
}
}
}
```
### Inline Loading
```kotlin
if (state.isLoading) {
MifosProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
```
### Overlay Loading
```kotlin
Box(modifier = Modifier.fillMaxSize()) {
// Content
[Screen]SuccessContent(...)
// Loading overlay
if (state.showOverlay) {
MifosLoadingDialog()
}
}
```
---
## Error States
### Full Screen Error
```kotlin
MifosErrorComponent(
message = stringResource(state.uiState.message),
onRetry = { onAction([Screen]Action.OnRetry) },
modifier = Modifier.padding(paddingValues),
)
```
### Inline Error
```kotlin
if (state.error != null) {
Text(
text = state.error,
color = KptTheme.colorScheme.error,
style = MifosTypography.bodySmall,
)
}
```
---
## Empty States
```kotlin
EmptyDataView(
icon = Icons.Default.Inbox,
title = stringResource(Res.string.no_data_title),
message = stringResource(Res.string.no_data_message),
modifier = Modifier.padding(paddingValues),
)
```
---
## Dialog Management
### Dialog State in ViewModel State
```kotlin
@Immutable
data class [Screen]State(
// ...
val dialogState: DialogState? = null,
) {
sealed interface DialogState {
data class Error(val message: String) : DialogState
data class ConfirmDelete(val item: Item) : DialogState
data object Success : DialogState
}
}
```
### Dialog Composable
```kotlin
@Composable
private fun [Screen]Dialogs(
dialogState: [Screen]State.DialogState?,
onDismiss: () -> Unit,
onConfirmDelete: () -> Unit = {},
) {
when (dialogState) {
is [Screen]State.DialogState.Error -> {
MifosBasicDialog(
visibilityState = BasicDialogState.Shown(
message = dialogState.message,
),
onDismissRequest = onDismiss,
)
}
is [Screen]State.DialogState.ConfirmDelete -> {
MifosAlertDialog(
title = stringResource(Res.string.confirm_delete),
message = stringResource(Res.string.delete_message),
confirmText = stringResource(Res.string.delete),
onConfirm = onConfirmDelete,
onDismiss = onDismiss,
)
}
is [Screen]State.DialogState.Success -> {
MifosSuccessDialog(
message = stringResource(Res.string.success_message),
onDismiss = onDismiss,
)
}
null -> Unit
}
}
```
---
## Theming Guidelines
### NEVER Use Hardcoded Values
```kotlin
// WRONG - Hardcoded values
Column(
modifier = Modifier.padding(16.dp) // Hardcoded
) {
Text(
text = "Title",
fontSize = 24.sp, // Hardcoded
color = Color(0xFF1A1A1A), // Hardcoded
)
}
// CORRECT - Theme tokens
Column(
modifier = Modifier.padding(KptTheme.spacing.md)
) {
Text(
text = stringResource(Res.string.title), // String resource
style = MifosTypography.titleLarge, // Typography token
color = KptTheme.colorScheme.onSurface, // Color token
)
}
```
### Theme Token Quick Reference
> Full theme tokens defined in [Available Components](#theme-tokens-always-available) section.
| Token Type | Access | Examples |
|------------|--------|----------|
| Spacing | `KptTheme.spacing.*` | `xs`, `sm`, `md`, `lg`, `xl` |
| Shapes | `KptTheme.shapes.*` | `small`, `medium`, `large` |
| Colors | `KptTheme.colorScheme.*` | `primary`, `surface`, `error` |
| Typography | `MifosTypography.*` | `titleLarge`, `bodyMedium` |
### Theming Rules
1. **NEVER** use hardcoded `dp`, `sp`, or `Color()` values
2. **ALWAYS** use `KptTheme.spacing.*` for padding/margin
3. **ALWAYS** use `KptTheme.shapes.*` for corner radius
4. **ALWAYS** use `KptTheme.colorScheme.*` for colors
5. **ALWAYS** use `MifosTypography.*` for text styles
6. **ALWAYS** use `stringResource()` for user-facing text
---
## Preview Patterns
### Content Preview
```kotlin
@Preview
@Composable
private fun [Screen]ContentPreview() {
MifosMobileTheme {
[Screen]Content(
state = [Screen]State(
uiState = [Screen]ScreenState.Success,
items = listOf(
Item(id = 1, name = "Item 1"),
Item(id = 2, name = "Item 2"),
),
),
snackbarHostState = remember { SnackbarHostState() },
onAction = {},
)
}
}
```
### State Previews
```kotlin
@Preview
@Composable
private fun [Screen]LoadingPreview() {
MifosMobileTheme {
[Screen]LoadingContent()
}
}
@Preview
@Composable
private fun [Screen]ErrorPreview() {
MifosMobileTheme {
MifosErrorComponent(
message = "Failed to load data",
onRetry = {},
)
}
}
@Preview
@Composable
private fun [Screen]EmptyPreview() {
MifosMobileTheme {
EmptyDataView()
}
}
```
### Component Preview
```kotlin
@Preview
@Composable
private fun [Screen]HeaderSectionPreview() {
MifosMobileTheme {
[Screen]HeaderSection(
title = "Account Details",
subtitle = "Savings Account",
)
}
}
```
---
## Checklist for New Screens
Before creating a new screen:
### Component Lookup (REQUIRED)
- [ ] **Step 1**: Check static registry in [core-layer/COMPONENTS.md](../../core-layer/COMPONENTS.md)
- [ ] **Step 2**: If not found, run dynamic search commands
- [ ] **Step 3**: If found dynamically, update the static registry
- [ ] Verify no duplicate exists before creating new component
### Screen Design
- [ ] Plan sections (Header, Content, Actions, List)
- [ ] Identify reusable components (don't create duplicates!)
- [ ] Follow naming conventions (`Kpt*`, `Mifos*`, `[Feature]*`)
### Implementation
- [ ] Follow Container + Content pattern
- [ ] Use theme tokens ONLY (no hardcoded values)
- [ ] Use `stringResource()` for all text
- [ ] Use `remember(viewModel)` for callbacks
### States
- [ ] Add loading state with `KptShimmerLoadingBox`
- [ ] Add error state with `MifosErrorComponent`
- [ ] Add empty state with `EmptyDataView`
- [ ] Add network error state with `NoInternet`
- [ ] Handle dialogs separately
### Verification
- [ ] Create previews for each state
- [ ] No hardcoded `dp`, `sp`, `Color()` values
- [ ] All strings in `strings.xml`

View File

@ -0,0 +1,282 @@
# Dependency Injection Patterns
## Table of Contents
1. [Koin Module](#koin-module)
2. [ViewModel Registration](#viewmodel-registration)
3. [Multiple ViewModels](#multiple-viewmodels)
4. [Module Registration](#module-registration)
5. [Accessing Dependencies](#accessing-dependencies)
6. [Complete Example](#complete-example)
---
## Koin Module
Create a Koin module for each feature:
```kotlin
// feature/[name]/di/[Feature]Module.kt
package org.mifos.mobile.feature.[name].di
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
import org.mifos.mobile.feature.[name].[Feature]ViewModel
val [Feature]Module = module {
viewModelOf(::[Feature]ViewModel)
}
```
---
## ViewModel Registration
### Single ViewModel
```kotlin
val [Feature]Module = module {
viewModelOf(::[Feature]ViewModel)
}
```
### ViewModel with SavedStateHandle
SavedStateHandle is automatically injected:
```kotlin
// ViewModel constructor
internal class [Feature]ViewModel(
private val repository: [Feature]Repository,
savedStateHandle: SavedStateHandle, // Auto-injected
) : BaseViewModel<...>(...) {
init {
// Access navigation arguments
val id = savedStateHandle.get<Long>("id")
}
}
// Module - no special registration needed
val [Feature]Module = module {
viewModelOf(::[Feature]ViewModel)
}
```
---
## Multiple ViewModels
Register all ViewModels in a feature module:
```kotlin
val AuthModule = module {
viewModelOf(::LoginViewModel)
viewModelOf(::RegistrationViewModel)
viewModelOf(::OtpAuthenticationViewModel)
viewModelOf(::RecoverPasswordViewModel)
viewModelOf(::SetPasswordViewModel)
}
```
---
## Module Registration
### Step 1: Export Module
```kotlin
// feature/[name]/di/[Feature]Module.kt
val [Feature]Module = module {
viewModelOf(::[Feature]ViewModel)
}
```
### Step 2: Register in KoinModules
```kotlin
// cmp-navigation/src/.../di/KoinModules.kt
val featureModules = module {
includes(
// Core modules
AuthModule,
HomeModule,
AccountsModule,
// Account modules
SavingsAccountModule,
LoanModule,
ShareAccountModule,
// Transaction modules
BeneficiaryModule,
TransferProcessModule,
ThirdPartyTransferModule,
// Utility modules
NotificationModule,
SettingsModule,
QrModule,
// Add your module here
[Feature]Module,
)
}
```
---
## Accessing Dependencies
### In ViewModel
Dependencies are constructor-injected:
```kotlin
internal class [Feature]ViewModel(
private val repository: [Feature]Repository,
private val userPreferences: UserPreferencesRepository,
private val networkMonitor: NetworkMonitor,
) : BaseViewModel<[Feature]State, [Feature]Event, [Feature]Action>(
initialState = [Feature]State()
) {
// Use injected dependencies
private fun loadData() {
viewModelScope.launch {
repository.getData().collect { ... }
}
}
}
```
### In Composable
Use `koinViewModel()`:
```kotlin
@Composable
internal fun [Feature]Screen(
onNavigateBack: () -> Unit,
modifier: Modifier = Modifier,
viewModel: [Feature]ViewModel = koinViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
// ...
}
```
### With ViewModel Key
For multiple instances of same ViewModel:
```kotlin
@Composable
fun [Feature]Screen(
accountId: Long,
viewModel: [Feature]ViewModel = koinViewModel(
key = "account_$accountId"
),
) {
// Unique ViewModel instance per accountId
}
```
---
## Complete Example
### Module Definition
```kotlin
// feature/beneficiary/di/BeneficiaryModule.kt
package org.mifos.mobile.feature.beneficiary.di
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
import org.mifos.mobile.feature.beneficiary.list.BeneficiaryListViewModel
import org.mifos.mobile.feature.beneficiary.detail.BeneficiaryDetailViewModel
import org.mifos.mobile.feature.beneficiary.add.AddBeneficiaryViewModel
import org.mifos.mobile.feature.beneficiary.edit.EditBeneficiaryViewModel
val BeneficiaryModule = module {
viewModelOf(::BeneficiaryListViewModel)
viewModelOf(::BeneficiaryDetailViewModel)
viewModelOf(::AddBeneficiaryViewModel)
viewModelOf(::EditBeneficiaryViewModel)
}
```
### ViewModel with Dependencies
```kotlin
// feature/beneficiary/list/BeneficiaryListViewModel.kt
internal class BeneficiaryListViewModel(
private val beneficiaryRepository: BeneficiaryRepository,
) : BaseViewModel<BeneficiaryListState, BeneficiaryListEvent, BeneficiaryListAction>(
initialState = BeneficiaryListState()
) {
init {
loadBeneficiaries()
}
override fun handleAction(action: BeneficiaryListAction) {
// ...
}
private fun loadBeneficiaries() {
viewModelScope.launch {
beneficiaryRepository.getBeneficiaries().collect { result ->
// ...
}
}
}
}
```
### Screen with ViewModel
```kotlin
// feature/beneficiary/list/BeneficiaryListScreen.kt
@Composable
internal fun BeneficiaryListScreen(
onNavigateBack: () -> Unit,
onNavigateToDetail: (Long) -> Unit,
modifier: Modifier = Modifier,
viewModel: BeneficiaryListViewModel = koinViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel.eventFlow) { event ->
when (event) {
is BeneficiaryListEvent.NavigateBack -> onNavigateBack()
is BeneficiaryListEvent.NavigateToDetail -> onNavigateToDetail(event.id)
}
}
BeneficiaryListContent(
state = state,
onAction = remember(viewModel) { { viewModel.trySendAction(it) } },
modifier = modifier,
)
}
```
### Registration in KoinModules
```kotlin
// cmp-navigation/src/.../di/KoinModules.kt
val featureModules = module {
includes(
AuthModule,
HomeModule,
AccountsModule,
BeneficiaryModule, // Registered here
// ...
)
}
```

View File

@ -0,0 +1,438 @@
# Navigation Patterns
## Table of Contents
1. [Route Definition](#route-definition)
2. [NavController Extensions](#navcontroller-extensions)
3. [NavGraphBuilder Extensions](#navgraphbuilder-extensions)
4. [Transitions](#transitions)
5. [Arguments](#arguments)
6. [Type-Safe Destinations](#type-safe-destinations)
7. [Navigation Graph](#navigation-graph)
8. [Complete Example](#complete-example)
---
## Route Definition
Use `@Serializable` data objects for type-safe navigation:
### Simple Route
```kotlin
@Serializable
data object [Feature]Route
```
### Route with Arguments
```kotlin
@Serializable
data class [Feature]DetailRoute(
val id: Long,
)
@Serializable
data class [Feature]EditRoute(
val id: Long,
val title: String? = null, // Optional argument
)
```
---
## NavController Extensions
Create extension functions for navigation:
### Simple Navigation
```kotlin
fun NavController.navigateTo[Feature]Screen(navOptions: NavOptions? = null) {
navigate([Feature]Route, navOptions)
}
```
### Navigation with Arguments
```kotlin
fun NavController.navigateTo[Feature]Detail(id: Long) {
navigate([Feature]DetailRoute(id))
}
fun NavController.navigateTo[Feature]Edit(id: Long, title: String? = null) {
navigate([Feature]EditRoute(id, title))
}
```
### Navigation with Pop Behavior
```kotlin
fun NavController.navigateTo[Feature]Screen() {
navigate([Feature]Route) {
popUpTo([Previous]Route) {
inclusive = true
}
}
}
```
---
## NavGraphBuilder Extensions
### Simple Screen
```kotlin
fun NavGraphBuilder.[feature]Screen(
onNavigateBack: () -> Unit,
onNavigateToDetail: (Long) -> Unit,
) {
composableWithStayTransitions<[Feature]Route> {
[Feature]Screen(
onNavigateBack = onNavigateBack,
onNavigateToDetail = onNavigateToDetail,
)
}
}
```
### Screen with Arguments
```kotlin
fun NavGraphBuilder.[feature]DetailScreen(
onNavigateBack: () -> Unit,
) {
composableWithStayTransitions<[Feature]DetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<[Feature]DetailRoute>()
[Feature]DetailScreen(
id = route.id,
onNavigateBack = onNavigateBack,
)
}
}
```
### Screen with Optional Arguments
```kotlin
fun NavGraphBuilder.[feature]EditScreen(
onNavigateBack: () -> Unit,
onSaveComplete: () -> Unit,
) {
composableWithStayTransitions<[Feature]EditRoute> { backStackEntry ->
val route = backStackEntry.toRoute<[Feature]EditRoute>()
[Feature]EditScreen(
id = route.id,
title = route.title,
onNavigateBack = onNavigateBack,
onSaveComplete = onSaveComplete,
)
}
}
```
---
## Transitions
### Standard Transitions
```kotlin
// Default stay transitions (recommended)
composableWithStayTransitions<[Feature]Route> {
[Feature]Screen(...)
}
```
### Custom Transitions
```kotlin
composable<[Feature]Route>(
enterTransition = {
slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(300)
)
},
exitTransition = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> -fullWidth },
animationSpec = tween(300)
)
},
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { fullWidth -> -fullWidth },
animationSpec = tween(300)
)
},
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(300)
)
},
) {
[Feature]Screen(...)
}
```
### Fade Transitions
```kotlin
composable<[Feature]Route>(
enterTransition = { fadeIn(animationSpec = tween(300)) },
exitTransition = { fadeOut(animationSpec = tween(300)) },
) {
[Feature]Screen(...)
}
```
---
## Arguments
### Primitive Types
```kotlin
@Serializable
data class [Feature]Route(
val id: Long,
val name: String,
val isEnabled: Boolean = true,
val count: Int = 0,
)
```
### Nullable Arguments
```kotlin
@Serializable
data class [Feature]Route(
val id: Long,
val title: String? = null, // Optional
)
```
### Enum Arguments
```kotlin
@Serializable
enum class AccountType {
SAVINGS, LOAN, SHARE
}
@Serializable
data class AccountRoute(
val accountType: AccountType,
)
```
### Accessing Arguments
```kotlin
composableWithStayTransitions<[Feature]DetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<[Feature]DetailRoute>()
[Feature]DetailScreen(
id = route.id,
title = route.title ?: "Default Title",
)
}
```
---
## Type-Safe Destinations
### Sealed Class Pattern
```kotlin
sealed class [Feature]Destination {
data object List : [Feature]Destination()
data class Detail(val id: Long) : [Feature]Destination()
data class Edit(val id: Long) : [Feature]Destination()
data object Create : [Feature]Destination()
}
typealias [Feature]Navigator = ([Feature]Destination) -> Unit
```
### Usage in Screen
```kotlin
@Composable
internal fun [Feature]Screen(
onNavigate: [Feature]Navigator,
onNavigateBack: () -> Unit,
) {
// Navigate to detail
onNavigate([Feature]Destination.Detail(itemId))
// Navigate to create
onNavigate([Feature]Destination.Create)
}
```
### Usage in NavGraph
```kotlin
fun NavGraphBuilder.[feature]Screen(
navController: NavController,
) {
composableWithStayTransitions<[Feature]Route> {
[Feature]Screen(
onNavigate = { destination ->
when (destination) {
is [Feature]Destination.Detail -> {
navController.navigateTo[Feature]Detail(destination.id)
}
is [Feature]Destination.Create -> {
navController.navigateTo[Feature]Create()
}
// ...
}
},
onNavigateBack = { navController.popBackStack() },
)
}
}
```
---
## Navigation Graph
### Feature Navigation Graph
```kotlin
// feature/[name]/navigation/[Feature]Navigation.kt
@Serializable
data object [Feature]GraphRoute
fun NavGraphBuilder.[feature]NavGraph(
navController: NavController,
) {
navigation<[Feature]GraphRoute>(
startDestination = [Feature]ListRoute,
) {
[feature]ListScreen(
onNavigateToDetail = { id ->
navController.navigateTo[Feature]Detail(id)
},
)
[feature]DetailScreen(
onNavigateBack = { navController.popBackStack() },
onNavigateToEdit = { id ->
navController.navigateTo[Feature]Edit(id)
},
)
[feature]EditScreen(
onNavigateBack = { navController.popBackStack() },
)
}
}
```
### Registering in Main Graph
```kotlin
// cmp-navigation/.../MainNavGraph.kt
NavHost(
navController = navController,
startDestination = HomeRoute,
) {
homeScreen(...)
[feature]NavGraph(navController)
// OR individual screens
[feature]Screen(...)
[feature]DetailScreen(...)
}
```
---
## Complete Example
```kotlin
// feature/beneficiary/navigation/BeneficiaryNavigation.kt
package org.mifos.mobile.feature.beneficiary.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.toRoute
import kotlinx.serialization.Serializable
import org.mifos.mobile.feature.beneficiary.list.BeneficiaryListScreen
import org.mifos.mobile.feature.beneficiary.detail.BeneficiaryDetailScreen
import org.mifos.mobile.feature.beneficiary.add.AddBeneficiaryScreen
// Routes
@Serializable
data object BeneficiaryListRoute
@Serializable
data class BeneficiaryDetailRoute(val id: Long)
@Serializable
data object AddBeneficiaryRoute
// NavController Extensions
fun NavController.navigateToBeneficiaryList(navOptions: NavOptions? = null) {
navigate(BeneficiaryListRoute, navOptions)
}
fun NavController.navigateToBeneficiaryDetail(id: Long) {
navigate(BeneficiaryDetailRoute(id))
}
fun NavController.navigateToAddBeneficiary() {
navigate(AddBeneficiaryRoute)
}
// NavGraphBuilder Extensions
fun NavGraphBuilder.beneficiaryListScreen(
onNavigateBack: () -> Unit,
onNavigateToDetail: (Long) -> Unit,
onNavigateToAdd: () -> Unit,
) {
composableWithStayTransitions<BeneficiaryListRoute> {
BeneficiaryListScreen(
onNavigateBack = onNavigateBack,
onNavigateToDetail = onNavigateToDetail,
onNavigateToAdd = onNavigateToAdd,
)
}
}
fun NavGraphBuilder.beneficiaryDetailScreen(
onNavigateBack: () -> Unit,
) {
composableWithStayTransitions<BeneficiaryDetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<BeneficiaryDetailRoute>()
BeneficiaryDetailScreen(
beneficiaryId = route.id,
onNavigateBack = onNavigateBack,
)
}
}
fun NavGraphBuilder.addBeneficiaryScreen(
onNavigateBack: () -> Unit,
onBeneficiaryAdded: () -> Unit,
) {
composableWithStayTransitions<AddBeneficiaryRoute> {
AddBeneficiaryScreen(
onNavigateBack = onNavigateBack,
onBeneficiaryAdded = onBeneficiaryAdded,
)
}
}
```

View File

@ -0,0 +1,309 @@
# Updating Existing Feature
## Table of Contents
1. [Overview](#overview)
2. [When to Use](#when-to-use)
3. [Pre-Update Checklist](#pre-update-checklist)
4. [Update Workflow](#update-workflow)
5. [Component Updates](#component-updates)
6. [State Migration](#state-migration)
7. [Testing Updates](#testing-updates)
8. [Post-Update Checklist](#post-update-checklist)
---
## Overview
This guide covers updating existing features for v2.0 UI redesign or improvements. Unlike creating new features, updates require careful consideration of:
- Existing state management
- Current navigation patterns
- Component reusability
- Backward compatibility
---
## When to Use
Use this guide when:
| Scenario | Example |
|----------|---------|
| **v2.0 UI Redesign** | Modernizing login screen with new design |
| **Adding Screens** | Adding detail screen to existing feature |
| **Refactoring** | Splitting monolithic screen into sections |
| **Component Migration** | Moving from custom to design system components |
| **Pattern Alignment** | Updating to Container + Content pattern |
---
## Pre-Update Checklist
Before starting updates:
- [ ] Read current design spec: `design-spec-layer/features/[feature]/SPEC.md`
- [ ] Review mockups: `design-spec-layer/features/[feature]/MOCKUP.md`
- [ ] Check API changes: `design-spec-layer/features/[feature]/API.md`
- [ ] Understand current implementation structure
- [ ] Identify reusable components
- [ ] Plan state changes (if any)
- [ ] Identify screens that can be updated independently
---
## Update Workflow
### Step 1: Analyze Current Implementation
```bash
# List current structure
ls -la feature/[name]/src/commonMain/kotlin/org/mifos/mobile/feature/[name]/
# Find all screens
find feature/[name]/ -name "*Screen.kt"
# Find all ViewModels
find feature/[name]/ -name "*ViewModel.kt"
```
### Step 2: Compare with Design Spec
| Current | Design Spec | Action |
|---------|-------------|--------|
| LoginScreen.kt | Login Screen v2.0 | Update UI |
| - | Biometric Login | Add new screen |
| OldComponent.kt | Removed | Delete |
### Step 3: Update Order
**Recommended order:**
1. **State/Action** - Add new fields without breaking existing
2. **ViewModel** - Add new handlers
3. **Components** - Create/update section components
4. **Screen** - Update layout
5. **Navigation** - Add new routes if needed
6. **DI** - Update if new dependencies
---
## Component Updates
### Replacing Hardcoded Values
```kotlin
// BEFORE - Hardcoded
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Title",
fontSize = 24.sp,
color = Color.Black,
)
}
// AFTER - Theme tokens
Column(
modifier = Modifier.padding(KptTheme.spacing.md)
) {
Text(
text = stringResource(Res.string.title),
style = MifosTypography.titleLarge,
color = KptTheme.colorScheme.onSurface,
)
}
```
### Replacing Custom Components
```kotlin
// BEFORE - Custom implementation
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.Gray)
.padding(16.dp)
) {
// Custom loading
}
// AFTER - Design system component
KptShimmerLoadingBox(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.clip(KptTheme.shapes.medium),
)
```
### Section Extraction
```kotlin
// BEFORE - Monolithic
@Composable
private fun AccountContent(state: State) {
Column {
// 200+ lines of mixed code
Text(state.title)
Text(state.balance)
// ... buttons, lists, etc.
}
}
// AFTER - Section-based
@Composable
private fun AccountSuccessContent(
state: State,
onAction: (Action) -> Unit,
) {
LazyColumn {
item { AccountHeaderSection(state.title) }
item { AccountBalanceSection(state.balance) }
item { AccountActionsSection(onAction) }
items(state.transactions) { TransactionItem(it) }
}
}
```
---
## State Migration
### Adding New Fields
```kotlin
// BEFORE
data class [Feature]State(
val items: List<Item> = emptyList(),
val isLoading: Boolean = false,
)
// AFTER - Add with defaults (non-breaking)
data class [Feature]State(
val items: List<Item> = emptyList(),
val isLoading: Boolean = false,
// New fields with defaults
val selectedFilter: FilterType = FilterType.ALL,
val searchQuery: String = "",
val dialogState: DialogState? = null,
)
```
### Adding New Actions
```kotlin
// BEFORE
sealed interface [Feature]Action {
data class OnItemClick(val id: Long) : [Feature]Action
}
// AFTER - Add new actions
sealed interface [Feature]Action {
data class OnItemClick(val id: Long) : [Feature]Action
// New actions
data class OnFilterChange(val filter: FilterType) : [Feature]Action
data class OnSearchQuery(val query: String) : [Feature]Action
data object OnClearSearch : [Feature]Action
}
```
### Adding New Events
```kotlin
// BEFORE
sealed interface [Feature]Event {
data object NavigateBack : [Feature]Event
}
// AFTER - Add new events
sealed interface [Feature]Event {
data object NavigateBack : [Feature]Event
// New events
data class ShowFilterSheet(val options: List<FilterType>) : [Feature]Event
data class ShowToast(val message: String) : [Feature]Event
}
```
---
## Testing Updates
### Verify No Regression
```bash
# Run feature tests
./gradlew :feature:[name]:test
# Run lint
./gradlew :feature:[name]:detekt
# Build feature
./gradlew :feature:[name]:build
```
### Manual Testing
- [ ] All existing flows still work
- [ ] New UI matches mockups
- [ ] Loading states display correctly
- [ ] Error states handle gracefully
- [ ] Empty states show appropriate message
- [ ] Navigation works as expected
- [ ] Dialogs open/close properly
---
## Post-Update Checklist
After completing updates:
- [ ] All screens follow Container + Content pattern
- [ ] All components use KptTheme tokens (no hardcoded values)
- [ ] All strings in strings.xml (no hardcoded text)
- [ ] Section components in `[screen]/components/`
- [ ] Shared components in `feature/[name]/components/`
- [ ] Previews added for all states
- [ ] Build passes: `./gradlew :feature:[name]:build`
- [ ] Update STATUS.md: `design-spec-layer/features/[feature]/STATUS.md`
- [ ] Update LAYER_STATUS.md if structure changed
---
## Common Update Patterns
### Pattern 1: Add Search to List Screen
1. Add state fields: `searchQuery`, `filteredItems`
2. Add actions: `OnSearchQuery`, `OnClearSearch`
3. Add `MifosSearchTextField` above list
4. Filter in ViewModel based on query
### Pattern 2: Add Detail Screen to Feature
1. Create `[feature]Detail/` package
2. Add `[Feature]DetailRoute` with argument
3. Add navigation extension function
4. Register in NavGraph
5. Update list screen to navigate on click
### Pattern 3: Convert to Tab Layout
1. Add `selectedTab` to state
2. Add `OnTabSelect` action
3. Use `MifosTabPager` or `TabRow`
4. Create content composable per tab
### Pattern 4: Add Pull-to-Refresh
1. Add `isRefreshing` to state
2. Add `OnRefresh` action
3. Wrap content with `PullToRefreshBox`
4. Call refresh in ViewModel
---
## Related Files
- New Feature: [LAYER_GUIDE.md](../LAYER_GUIDE.md#creating-new-feature)
- Compose Patterns: [COMPOSE.md](COMPOSE.md)
- ViewModel Patterns: [VIEWMODEL.md](VIEWMODEL.md)
- Navigation: [NAVIGATION.md](NAVIGATION.md)

View File

@ -0,0 +1,495 @@
# ViewModel Patterns
## Table of Contents
1. [BaseViewModel](#baseviewmodel)
2. [State Definition](#state-definition)
3. [ScreenState Definition](#screenstate-definition)
4. [Event Definition](#event-definition)
5. [Action Definition](#action-definition)
6. [Internal Actions](#internal-actions)
7. [handleAction Pattern](#handleaction-pattern)
8. [State Updates](#state-updates)
9. [Async Operations](#async-operations)
10. [Complete Example](#complete-example)
---
## BaseViewModel
All ViewModels extend `BaseViewModel<State, Event, Action>`:
```kotlin
internal class [Feature]ViewModel(
private val repository: [Feature]Repository,
) : BaseViewModel<[Feature]State, [Feature]Event, [Feature]Action>(
initialState = [Feature]State()
) {
override fun handleAction(action: [Feature]Action) {
// Handle user actions
}
}
```
**Generic Parameters:**
- `State` - UI state data class
- `Event` - One-shot effects (navigation, toasts)
- `Action` - User interactions
---
## State Definition
Immutable data class holding UI state:
```kotlin
@Immutable
data class [Feature]State(
val isLoading: Boolean = false,
val items: List<Item> = emptyList(),
val selectedItem: Item? = null,
val dialogState: DialogState? = null,
val uiState: [Feature]ScreenState = [Feature]ScreenState.Loading,
) {
// Derived properties
val hasItems: Boolean get() = items.isNotEmpty()
val isButtonEnabled: Boolean get() = selectedItem != null
// Nested sealed interface for dialogs
sealed interface DialogState {
data class Error(val message: String) : DialogState
data class Confirm(val item: Item) : DialogState
}
}
```
**Rules:**
- Always use `@Immutable` annotation
- Use default values for all properties
- Derived properties via `get()` for computed values
- Nested `DialogState` for dialog management
---
## ScreenState Definition
Loading/Success/Error states:
```kotlin
sealed interface [Feature]ScreenState {
data object Loading : [Feature]ScreenState
data object Success : [Feature]ScreenState
data object Empty : [Feature]ScreenState
data class Error(val message: StringResource) : [Feature]ScreenState
data object Network : [Feature]ScreenState
}
```
**Standard States:**
| State | Purpose |
|-------|---------|
| `Loading` | Initial load or refresh |
| `Success` | Data loaded successfully |
| `Empty` | No data available |
| `Error` | Error with message |
| `Network` | No internet connection |
---
## Event Definition
One-shot effects (navigation, toasts):
```kotlin
sealed interface [Feature]Event {
data object NavigateBack : [Feature]Event
data class NavigateToDetail(val id: Long) : [Feature]Event
data class ShowToast(val message: String) : [Feature]Event
data object ShowSuccess : [Feature]Event
}
```
**When to Use Events:**
- Navigation between screens
- Showing snackbars/toasts
- One-time UI effects
- NOT for state changes (use State instead)
---
## Action Definition
User interactions:
```kotlin
sealed interface [Feature]Action {
// User-initiated actions
data object OnRefresh : [Feature]Action
data object OnRetry : [Feature]Action
data object OnNavigateBack : [Feature]Action
data class OnItemClick(val id: Long) : [Feature]Action
data class OnSearchQuery(val query: String) : [Feature]Action
data object OnDismissDialog : [Feature]Action
data class OnDeleteItem(val id: Long) : [Feature]Action
// Internal actions for async results
sealed interface Internal : [Feature]Action {
data class ReceiveData(val result: DataState<Data>) : Internal
data class ReceiveDeleteResult(val result: DataState<Unit>) : Internal
}
}
```
**Naming Convention:**
- Prefix with `On` for user actions
- Use present tense (`OnClick`, not `Clicked`)
---
## Internal Actions
Handle async operation results:
```kotlin
sealed interface Internal : [Feature]Action {
data class ReceiveData(val result: DataState<Data>) : Internal
data class ReceiveDeleteResult(val result: DataState<Unit>) : Internal
data class ReceiveUpdateResult(val result: DataState<Item>) : Internal
}
```
**Purpose:**
- Decouple async operations from state updates
- Testable state transitions
- Clear separation of concerns
**Usage in ViewModel:**
```kotlin
private fun loadData() {
viewModelScope.launch {
repository.getData().collect { result ->
sendAction([Feature]Action.Internal.ReceiveData(result))
}
}
}
override fun handleAction(action: [Feature]Action) {
when (action) {
is [Feature]Action.Internal.ReceiveData -> handleDataResult(action.result)
// ...
}
}
```
---
## handleAction Pattern
Central action handler:
```kotlin
override fun handleAction(action: [Feature]Action) {
when (action) {
// Navigation actions
is [Feature]Action.OnNavigateBack -> {
sendEvent([Feature]Event.NavigateBack)
}
// Data actions
is [Feature]Action.OnRefresh -> loadData()
is [Feature]Action.OnRetry -> loadData()
// Item actions
is [Feature]Action.OnItemClick -> {
sendEvent([Feature]Event.NavigateToDetail(action.id))
}
is [Feature]Action.OnDeleteItem -> deleteItem(action.id)
// Dialog actions
is [Feature]Action.OnDismissDialog -> {
updateState { it.copy(dialogState = null) }
}
// Internal actions
is [Feature]Action.Internal.ReceiveData -> {
handleDataResult(action.result)
}
is [Feature]Action.Internal.ReceiveDeleteResult -> {
handleDeleteResult(action.result)
}
}
}
```
**Best Practices:**
- Keep `when` branches concise
- Delegate complex logic to private functions
- Group related actions together
---
## State Updates
Use `updateState` for immutable updates:
```kotlin
private fun updateState(update: ([Feature]State) -> [Feature]State) {
mutableStateFlow.update(update)
}
// Simple update
updateState { it.copy(isLoading = true) }
// Multiple properties
updateState { state ->
state.copy(
items = newItems,
uiState = [Feature]ScreenState.Success,
isLoading = false
)
}
// Conditional update
updateState { state ->
state.copy(
uiState = if (items.isEmpty()) {
[Feature]ScreenState.Empty
} else {
[Feature]ScreenState.Success
}
)
}
```
---
## Async Operations
### Basic Pattern
```kotlin
private var loadJob: Job? = null
private fun loadData() {
loadJob?.cancel()
loadJob = viewModelScope.launch {
updateState { it.copy(uiState = [Feature]ScreenState.Loading) }
repository.getData()
.collect { result ->
sendAction([Feature]Action.Internal.ReceiveData(result))
}
}
}
```
### Handling Results
```kotlin
private fun handleDataResult(result: DataState<List<Item>>) {
when (result) {
is DataState.Loading -> {
updateState { it.copy(uiState = [Feature]ScreenState.Loading) }
}
is DataState.Success -> {
updateState { state ->
state.copy(
items = result.data,
uiState = if (result.data.isEmpty()) {
[Feature]ScreenState.Empty
} else {
[Feature]ScreenState.Success
}
)
}
}
is DataState.Error -> {
updateState { state ->
state.copy(
uiState = [Feature]ScreenState.Error(
Res.string.error_loading_data
)
)
}
}
}
}
```
### Multiple Operations
```kotlin
private var loadJob: Job? = null
private var deleteJob: Job? = null
private fun deleteItem(id: Long) {
deleteJob?.cancel()
deleteJob = viewModelScope.launch {
updateState { it.copy(isDeleting = true) }
repository.deleteItem(id)
.collect { result ->
sendAction([Feature]Action.Internal.ReceiveDeleteResult(result))
}
}
}
```
---
## Complete Example
```kotlin
// [Feature].kt - State/Event/Action definitions
@Immutable
data class [Feature]State(
val items: List<Item> = emptyList(),
val selectedItem: Item? = null,
val dialogState: DialogState? = null,
val uiState: [Feature]ScreenState = [Feature]ScreenState.Loading,
) {
val hasItems: Boolean get() = items.isNotEmpty()
sealed interface DialogState {
data class Error(val message: String) : DialogState
data class ConfirmDelete(val item: Item) : DialogState
}
}
sealed interface [Feature]ScreenState {
data object Loading : [Feature]ScreenState
data object Success : [Feature]ScreenState
data object Empty : [Feature]ScreenState
data class Error(val message: StringResource) : [Feature]ScreenState
}
sealed interface [Feature]Event {
data object NavigateBack : [Feature]Event
data class NavigateToDetail(val id: Long) : [Feature]Event
data class ShowToast(val message: String) : [Feature]Event
}
sealed interface [Feature]Action {
data object OnRefresh : [Feature]Action
data object OnRetry : [Feature]Action
data class OnItemClick(val id: Long) : [Feature]Action
data class OnDeleteClick(val item: Item) : [Feature]Action
data object OnConfirmDelete : [Feature]Action
data object OnDismissDialog : [Feature]Action
sealed interface Internal : [Feature]Action {
data class ReceiveData(val result: DataState<List<Item>>) : Internal
data class ReceiveDeleteResult(val result: DataState<Unit>) : Internal
}
}
```
```kotlin
// [Feature]ViewModel.kt
internal class [Feature]ViewModel(
private val repository: [Feature]Repository,
) : BaseViewModel<[Feature]State, [Feature]Event, [Feature]Action>(
initialState = [Feature]State()
) {
private var loadJob: Job? = null
private var deleteJob: Job? = null
init {
loadData()
}
override fun handleAction(action: [Feature]Action) {
when (action) {
is [Feature]Action.OnRefresh -> loadData()
is [Feature]Action.OnRetry -> loadData()
is [Feature]Action.OnItemClick -> {
sendEvent([Feature]Event.NavigateToDetail(action.id))
}
is [Feature]Action.OnDeleteClick -> {
updateState {
it.copy(dialogState = [Feature]State.DialogState.ConfirmDelete(action.item))
}
}
is [Feature]Action.OnConfirmDelete -> {
val item = (state.dialogState as? [Feature]State.DialogState.ConfirmDelete)?.item
item?.let { deleteItem(it.id) }
updateState { it.copy(dialogState = null) }
}
is [Feature]Action.OnDismissDialog -> {
updateState { it.copy(dialogState = null) }
}
is [Feature]Action.Internal.ReceiveData -> {
handleDataResult(action.result)
}
is [Feature]Action.Internal.ReceiveDeleteResult -> {
handleDeleteResult(action.result)
}
}
}
private fun loadData() {
loadJob?.cancel()
loadJob = viewModelScope.launch {
repository.getData().collect { result ->
sendAction([Feature]Action.Internal.ReceiveData(result))
}
}
}
private fun deleteItem(id: Long) {
deleteJob?.cancel()
deleteJob = viewModelScope.launch {
repository.deleteItem(id).collect { result ->
sendAction([Feature]Action.Internal.ReceiveDeleteResult(result))
}
}
}
private fun handleDataResult(result: DataState<List<Item>>) {
when (result) {
is DataState.Loading -> {
updateState { it.copy(uiState = [Feature]ScreenState.Loading) }
}
is DataState.Success -> {
updateState { state ->
state.copy(
items = result.data,
uiState = if (result.data.isEmpty()) {
[Feature]ScreenState.Empty
} else {
[Feature]ScreenState.Success
}
)
}
}
is DataState.Error -> {
updateState {
it.copy(uiState = [Feature]ScreenState.Error(Res.string.error_loading))
}
}
}
}
private fun handleDeleteResult(result: DataState<Unit>) {
when (result) {
is DataState.Loading -> { /* Optional: show loading */ }
is DataState.Success -> {
sendEvent([Feature]Event.ShowToast("Item deleted"))
loadData() // Refresh list
}
is DataState.Error -> {
updateState {
it.copy(dialogState = [Feature]State.DialogState.Error(result.message))
}
}
}
}
}
```