# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. **⚠️ Important:** This is the KMM mobile client repository. For full-stack documentation covering both the mobile app and backend API, see the root CLAUDE.md at `../CLAUDE.md`. ## Important Guidelines **⚠️ DO NOT auto-commit code changes.** Always ask the user before committing. Only create commits when the user explicitly requests it with commands like "commit this work" or "create a commit". ## Project Overview MyCrib is a Kotlin Multiplatform Mobile (KMM) property management application with shared business logic and platform-specific UI implementations. The backend is a Django REST Framework API (located in the sibling `myCribAPI` directory). **Tech Stack:** - **Shared (Kotlin)**: Compose Multiplatform for Android, networking layer, ViewModels, models - **iOS**: SwiftUI with Kotlin shared layer integration via SKIE - **Backend**: Django REST Framework (separate repository at `../myCribAPI`) ## Build Commands ### Android ```bash # Build debug APK ./gradlew :composeApp:assembleDebug # Build release APK ./gradlew :composeApp:assembleRelease # Run on connected device/emulator ./gradlew :composeApp:installDebug ``` ### iOS ```bash # Build from command line (use Xcode for best experience) xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17' build # Or open in Xcode open iosApp/iosApp.xcodeproj ``` ### Desktop (JVM) ```bash ./gradlew :composeApp:run ``` ### Web ```bash # Wasm target (modern browsers) ./gradlew :composeApp:wasmJsBrowserDevelopmentRun # JS target (older browser support) ./gradlew :composeApp:jsBrowserDevelopmentRun ``` ## Architecture ### Shared Kotlin Layer (`composeApp/src/commonMain/kotlin/com/example/mycrib/`) **Core Components:** 1. **APILayer** (`network/APILayer.kt`) - **Single entry point for all API calls** - Manages caching via DataCache - Handles automatic cache updates on mutations (create/update/delete) - Pattern: Cache-first reads with optional `forceRefresh` parameter - Returns `ApiResult` (Success/Error/Loading states) 2. **DataCache** (`cache/DataCache.kt`) - In-memory cache for lookup data (residence types, task categories, priorities, statuses, etc.) - Must be initialized via `APILayer.initializeLookups()` after login - Stores `MutableState` objects that UI can observe directly - Cleared on logout 3. **TokenStorage** (`storage/TokenStorage.kt`) - Platform-specific secure token storage - Android: EncryptedSharedPreferences - iOS: Keychain - All API calls automatically include token from TokenStorage 4. **ViewModels** (`viewmodel/`) - Shared ViewModels expose StateFlow for UI observation - Pattern: ViewModel calls APILayer → APILayer manages cache + network → ViewModel emits ApiResult states - ViewModels: `ResidenceViewModel`, `TaskViewModel`, `AuthViewModel`, `ContractorViewModel`, etc. 5. **Navigation** (`navigation/`) - Type-safe navigation using kotlinx.serialization - Routes defined as `@Serializable` data classes - Shared between Android Compose Navigation **Data Flow:** ``` UI → ViewModel → APILayer → (Cache Check) → Network API → Update Cache → Return to ViewModel → UI observes StateFlow ``` ### iOS Layer (`iosApp/iosApp/`) **Integration Pattern:** - SwiftUI views wrap Kotlin ViewModels via `@StateObject` - iOS-specific ViewModels (Swift) wrap shared Kotlin ViewModels - Pattern: `@Published var data` in Swift observes Kotlin `StateFlow` via async iteration - Navigation uses SwiftUI `NavigationStack` with sheets for modals **Key iOS Files:** - `MainTabView.swift`: Tab-based navigation - `*ViewModel.swift` (Swift): Wraps shared Kotlin ViewModels, exposes `@Published` properties - `*View.swift`: SwiftUI screens - Directory structure mirrors feature organization (Residence/, Task/, Contractor/, etc.) **iOS ↔ Kotlin Bridge:** ```swift // Swift ViewModel wraps Kotlin ViewModel @StateObject private var viewModel = ResidenceViewModel() // Swift wrapper // Inside: let sharedViewModel: ComposeApp.ResidenceViewModel // Kotlin // Observe Kotlin StateFlow Task { for await state in sharedViewModel.residencesState { await MainActor.run { self.residences = (state as? ApiResultSuccess)?.data } } } ``` ### Android Layer Android uses Compose UI directly from `composeApp` with shared ViewModels. Navigation via Jetpack Compose Navigation in `App.kt`. ## Environment Configuration **API Environment Toggle** (`composeApp/src/commonMain/kotlin/com/example/mycrib/network/ApiConfig.kt`): ```kotlin val CURRENT_ENV = Environment.DEV // or Environment.LOCAL ``` - `Environment.LOCAL`: Points to `http://10.0.2.2:8000/api` (Android emulator) or `http://127.0.0.1:8000/api` (iOS simulator) - `Environment.DEV`: Points to `https://mycrib.treytartt.com/api` **Change this to switch between local Django backend and production server.** ## Common Development Patterns ### Adding a New API Endpoint 1. Add API call to appropriate `*Api.kt` class in `network/` (e.g., `TaskApi.kt`) 2. Add method to `APILayer.kt` that manages caching (if applicable) 3. Add method to relevant ViewModel that calls APILayer 4. Update UI to observe the new StateFlow ### Handling Platform-Specific Code Use `expect/actual` pattern: ```kotlin // commonMain expect fun platformSpecificFunction(): String // androidMain actual fun platformSpecificFunction(): String = "Android" // iosMain actual fun platformSpecificFunction(): String = "iOS" ``` ### Type Conversions for iOS Kotlin types bridge to Swift with special wrappers: - `Double` → `KotlinDouble` (use `KotlinDouble(double:)` constructor) - `Int` → `KotlinInt` (use `KotlinInt(int:)` constructor) - `String` stays `String` - Optional types: Kotlin nullable (`Type?`) becomes Swift optional (`Type?`) **Example iOS form submission:** ```swift // TextField uses String binding @State private var estimatedCost: String = "" // Convert to KotlinDouble for API estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0) ``` ### Refreshing Lists After Mutations **iOS Pattern:** ```swift .sheet(isPresented: $showingAddForm) { AddFormView( isPresented: $showingAddForm, onSuccess: { viewModel.loadData(forceRefresh: true) } ) } ``` **Android Pattern:** ```kotlin // Use savedStateHandle to pass refresh flag between screens navController.previousBackStackEntry?.savedStateHandle?.set("refresh", true) navController.popBackStack() // In destination composable val shouldRefresh = backStackEntry.savedStateHandle.get("refresh") ?: false LaunchedEffect(shouldRefresh) { if (shouldRefresh) viewModel.loadData(forceRefresh = true) } ``` ## Testing Currently tests are minimal. When adding tests: - Android: Place in `composeApp/src/androidUnitTest/` or `composeApp/src/commonTest/` - iOS: Use XCTest framework in Xcode project ## Key Dependencies - Kotlin Multiplatform: 2.1.0 - Compose Multiplatform: 1.7.1 - Ktor Client: Network requests - kotlinx.serialization: JSON serialization - kotlinx.coroutines: Async operations - SKIE: Kotlin ↔ Swift interop improvements ## Important Notes ### Committing Changes When committing changes that span both iOS and Android, commit them together in the KMM repository. If backend changes are needed, commit separately in the `myCribAPI` repository. ### Data Cache Initialization **Critical**: After user login, call `APILayer.initializeLookups()` to populate DataCache with reference data. Without this, dropdowns and pickers will be empty. ```kotlin // After successful login val initResult = APILayer.initializeLookups() if (initResult is ApiResult.Success) { // Navigate to main screen } ``` ### iOS Build Issues If iOS build fails with type mismatch errors: 1. Check that cost fields (estimatedCost, actualCost, purchasePrice) use `KotlinDouble`, not `String` 2. Verify preview/mock data matches current model signatures 3. Clean build folder in Xcode (Cmd+Shift+K) and rebuild ### Force Refresh Pattern Always use `forceRefresh: true` when data should be fresh: - After creating/updating/deleting items - On pull-to-refresh gestures - When explicitly requested by user Without `forceRefresh`, APILayer returns cached data. ## Project Structure Summary ``` MyCribKMM/ ├── composeApp/ │ └── src/ │ ├── commonMain/kotlin/com/example/mycrib/ │ │ ├── cache/ # DataCache │ │ ├── models/ # Shared data models │ │ ├── network/ # APILayer, API clients │ │ ├── repository/ # Additional data repositories │ │ ├── storage/ # TokenStorage │ │ ├── ui/ # Compose UI (Android) │ │ │ ├── components/ # Reusable components │ │ │ ├── screens/ # Screen composables │ │ │ └── theme/ # Material theme │ │ ├── viewmodel/ # Shared ViewModels │ │ └── App.kt # Android navigation │ ├── androidMain/ # Android-specific code │ ├── iosMain/ # iOS-specific Kotlin code │ └── commonTest/ # Shared tests │ ├── iosApp/iosApp/ │ ├── *ViewModel.swift # Swift wrappers for Kotlin VMs │ ├── *View.swift # SwiftUI screens │ ├── Components/ # Reusable SwiftUI components │ ├── Design/ # Design system (spacing, colors) │ ├── Extensions/ # Swift extensions │ ├── Helpers/ # Utility helpers │ ├── PushNotifications/ # APNs integration │ └── [Feature]/ # Feature-grouped files │ ├── Task/ │ ├── Residence/ │ ├── Contractor/ │ └── Documents/ │ └── gradle/ # Gradle wrapper and configs ``` ## Related Repositories - **Backend API**: `../myCribAPI` - Django REST Framework backend - **Load Testing**: `../myCribAPI/locust` - Locust load testing scripts - **Documentation**: `../myCribAPI/docs` - Server configuration guides