Skip to main content

OdontoX Android — Native Kotlin + Jetpack Compose Production Spec

Date: 2026-05-09 Status: Draft for review Platform: Android 9+ (API 28+) / Kotlin 2.x / Jetpack Compose Tooling: Android Studio (Narwhal 2025.3 / latest 2026 stable) + Gemini AI assistance Roles in scope: Patient · Doctor · Admin · Receptionist Backend: Existing Hono/Cloudflare Workers API (api.odontox.io/api/v1/*)

1. Why Native Kotlin + Compose

CriterionReason
Material 3 ExpressiveGoogle’s latest design language (2026, announced at Google I/O 2025) — dynamic color, adaptive layouts, research-backed motion — first-class in Compose
PerformanceNo JS bridge; Compose compiler generates optimized bytecode; skippable composables
SecurityEncryptedSharedPreferences, Android Keystore, BiometricPrompt — direct framework APIs
PHI sensitivityNetwork Security Config enforced at OS level; no third-party runtime
Tablet / foldableNavigationSuiteScaffold + ListDetailPaneScaffold — adaptive layouts out-of-the-box
Android Studio + GeminiGemini-assisted code completion, refactoring, and test generation natively in IDE

2. System Architecture

Layer responsibilities

LayerOwnsNever does
ComposableUI state → pixels; user events → ViewModelBusiness logic, coroutine launch
ViewModelHold StateFlow<UiState>, map domain → UI model, handle eventsDirect network/DB access
Use CaseOne business operation as a suspend funUI concerns, DI
RepositorySSOT decision (local vs. remote), cache coordinationBusiness rules
Remote SourceRetrofit calls, OkHttp interceptors, DTO decodingCaching, mapping
Local SourceRoom DAOs, DataStore, EncryptedSharedPreferencesSync decisions

3. Architecture Pattern — Clean MVVM (Google MAD)

Google’s Modern Android Development guide (2026) recommends: MVVM + Repository + Hilt + Kotlin Coroutines + StateFlow. This is the canonical pattern. Unidirectional data flow (UDF):
  • UI emits events → ViewModel
  • ViewModel emits state → UI (one-way, no two-way binding)
  • Business logic never in Composables
  • UiState is a sealed class: Loading, Success(data), Error(message)

4. Project Structure

odontox-android/
├── app/
│   ├── src/main/
│   │   ├── java/io/odontox/app/
│   │   │   ├── OdontoXApp.kt              ← @HiltAndroidApp
│   │   │   ├── MainActivity.kt            ← @AndroidEntryPoint, single activity
│   │   │   │
│   │   │   ├── core/
│   │   │   │   ├── network/
│   │   │   │   │   ├── ApiClient.kt       ← Retrofit instance builder
│   │   │   │   │   ├── AuthInterceptor.kt ← OkHttp: attach JWT
│   │   │   │   │   ├── RefreshInterceptor.kt ← OkHttp: silent token refresh on 401
│   │   │   │   │   ├── ApiService.kt      ← Retrofit @GET/@POST interface
│   │   │   │   │   └── NetworkMonitor.kt  ← ConnectivityManager Flow
│   │   │   │   ├── cache/
│   │   │   │   │   ├── MemoryCache.kt     ← LruCache<String, Any> wrapper
│   │   │   │   │   ├── DiskCache.kt       ← Room + TTL-aware cache table
│   │   │   │   │   └── CachePolicy.kt     ← stale-while-revalidate logic
│   │   │   │   ├── auth/
│   │   │   │   │   ├── TokenManager.kt    ← EncryptedSharedPreferences: JWT, refresh
│   │   │   │   │   ├── PINManager.kt      ← SHA-256 + device salt
│   │   │   │   │   ├── BiometricHelper.kt ← BiometricPrompt wrapper
│   │   │   │   │   └── SessionManager.kt  ← StateFlow<AuthState>
│   │   │   │   ├── di/
│   │   │   │   │   ├── NetworkModule.kt   ← @Module Retrofit, OkHttp
│   │   │   │   │   ├── DatabaseModule.kt  ← @Module Room
│   │   │   │   │   └── RepositoryModule.kt ← @Binds interface → impl
│   │   │   │   └── security/
│   │   │   │       ├── ScreenshotPrevention.kt ← FLAG_SECURE
│   │   │   │       └── CertPinner.kt      ← OkHttp CertificatePinner
│   │   │   │
│   │   │   ├── features/
│   │   │   │   ├── auth/
│   │   │   │   │   ├── LoginScreen.kt
│   │   │   │   │   ├── PINScreen.kt
│   │   │   │   │   └── AuthViewModel.kt
│   │   │   │   ├── patient/
│   │   │   │   │   ├── home/
│   │   │   │   │   ├── appointments/
│   │   │   │   │   ├── records/
│   │   │   │   │   ├── bills/
│   │   │   │   │   └── chat/
│   │   │   │   ├── doctor/
│   │   │   │   ├── admin/
│   │   │   │   ├── receptionist/
│   │   │   │   └── shared/
│   │   │   │       ├── components/        ← AppointmentCard, StatusBadge, etc.
│   │   │   │       └── theme/             ← MaterialTheme, brand colors
│   │   │   │
│   │   │   ├── domain/
│   │   │   │   ├── entities/              ← Appointment.kt, Patient.kt, etc.
│   │   │   │   ├── usecases/
│   │   │   │   │   ├── BookAppointmentUseCase.kt
│   │   │   │   │   ├── CancelAppointmentUseCase.kt
│   │   │   │   │   └── …
│   │   │   │   └── repositories/          ← Repository interfaces
│   │   │   │
│   │   │   └── data/
│   │   │       ├── remote/
│   │   │       │   ├── dto/               ← data class matching API JSON
│   │   │       │   └── mappers/           ← DTO → Entity extension functions
│   │   │       ├── local/
│   │   │       │   ├── room/
│   │   │       │   │   ├── OdontoXDatabase.kt  ← @Database
│   │   │       │   │   ├── AppointmentDao.kt
│   │   │       │   │   └── CacheEntryDao.kt
│   │   │       │   └── datastore/
│   │   │       │       └── UserPreferencesStore.kt ← Proto DataStore
│   │   │       └── repositories/          ← Concrete implementations
│   │   │
│   │   ├── res/
│   │   │   ├── values/colors.xml          ← brand palette
│   │   │   ├── xml/network_security_config.xml
│   │   │   └── …
│   │   └── AndroidManifest.xml
│   │
│   └── build.gradle.kts

├── gradle/libs.versions.toml              ← version catalog
└── build.gradle.kts

5. Navigation Architecture

Google’s official recommendation for Compose navigation in 2026: Navigation 3 (stable 1.1.1, released April 22 2026) — full back-stack control, adaptive layouts, multi-pane support, shared-element transitions between scenes. Implementation notes:
  • Single MainActivity — no multiple Activity transitions
  • NavigationSuiteScaffold auto-switches between BottomBar / NavigationRail / Drawer based on WindowSizeClass
  • Navigation 3’s NavDisplay with typed back-stack keys — no string-based routes
  • ListDetailPaneScaffold for appointments list + detail on tablets
  • FLAG_SECURE set in MainActivity.onCreate() — persists for all screens

6. Auth Flow

Security rules (Google best practices):
ConcernImplementation
Token storageEncryptedSharedPreferences (AES-256-GCM key in Android Keystore) — never plain SharedPreferences
mPIN storageSHA-256(pin + UUID deviceSalt) in EncryptedSharedPreferences
BiometricBiometricPrompt with BIOMETRIC_STRONG requirement — fallback to PIN, not device lock
Session lockProcessLifecycleOwner.lifecycle ON_STOP → start 15-min countdown coroutine; ON_START → check elapsed
5 PIN failuresForce logout, wipe EncryptedSharedPreferences and Room DB
Network Security Confignetwork-security-config.xml — pins api.odontox.io cert, blocks cleartext
ScreenshotWindowManager.LayoutParams.FLAG_SECURE in MainActivity.onCreate()
Root detectionRootBeer library or manual check — log to backend, restrict PHI

7. Three-Tier Caching Strategy

Follows Google’s offline-first recommended pattern: Room DB as Single Source of Truth (SSOT), with LruCache for in-memory and WorkManager for background sync. The key insight: Room emits a new Flow value whenever its data changes. The ViewModel never polls — it just observes. When the network fetch updates Room, the UI automatically re-renders. This is Google’s SSOT pattern.

Tier 1 — Memory (LruCache)

  • LruCache<String, Any> keyed by "{entity}/{id}/{version}"
  • Max size: 4 MB (1/8 of available memory heuristic)
  • TTL: appointments 5 min, patient profile 30 min
  • Checked before Room query — eliminates deserialization overhead for hot paths

Tier 2 — Room (Disk — SSOT)

  • Entity tables mirror domain models (non-PHI fields only for structural data)
  • CacheEntry table: key TEXT PK, json TEXT, expiresAt INTEGER, version TEXT
  • PHI fields (clinical notes, prescriptions) use encrypted JSON via SQLCipher or store in EncryptedSharedPreferences instead of Room
  • @Query("SELECT * FROM cache_entries WHERE key = :key AND expiresAt > :now") — freshness enforced in SQL

Tier 3 — WorkManager (Background Sync)

  • SyncWorker with Constraints(requiresNetwork = true)
  • Differential sync: include updatedSince timestamp in requests — only changed records transferred
  • Retry policy: exponential backoff (1 min, 2 min, 4 min) on failure
  • Scheduled: every 15 min when app is visible, every 6 h in background (battery-aware)

TTL Policy

Data typeLruCacheRoomPHI stored
Appointment list5 min2 minNo
Patient profile30 min30 minNo (name/DOB excluded from Room)
Clinical notes5 minNeverMemory only
Prescriptions5 minNeverMemory only
Invoice list10 min10 minNo
PDF signed URLs4 minNeverMemory only
Clinic info / slots24 h24 hNo
NotificationsOn receivePermanentNo

Stale-While-Revalidate

1. Repository.fetchAppointments():
   a. Emit current Room data immediately → UI shows content
   b. Launch background coroutine: GET /appointments
   c. On response: update Room → Room Flow emits → UI refreshes
   d. User sees fresh data within seconds, no blank loading state

8. Networking Layer

OkHttp configuration:
  • OkHttpClient.Builder() with:
    • AuthInterceptor (attach JWT header)
    • RefreshInterceptor (catch 401, refresh, retry once)
    • CertificatePinner("api.odontox.io", "sha256/…") — protects against MitM on clinic WiFi
    • connectTimeout(30, SECONDS), readTimeout(30, SECONDS)
    • HttpLoggingInterceptor(Level.NONE) in production, Level.BODY in debug only
  • Retrofit with GsonConverterFactory (or Moshi for faster parsing)
  • @Streaming on PDF download endpoints — stream directly to temp file, never fully buffered

9. Local Persistence — Room + DataStore

Room — structural, non-PHI data: Proto DataStore — user preferences (replaces SharedPreferences):
  • biometric_enrolled: bool
  • theme_mode: ThemeMode (SYSTEM / LIGHT / DARK)
  • notification_opt_ins: repeated string
  • last_sync_timestamp: int64
PHI never written to Room or DataStore — memory-only caching or EncryptedSharedPreferences.

10. Dependency Injection — Hilt

Google’s standard DI framework for Android (replaces manual DI, Dagger boilerplate).
  • @Singleton scope for Retrofit, OkHttpClient, Room, TokenManager
  • @ViewModelScoped for Use Cases (one instance per ViewModel)
  • All ViewModels annotated @HiltViewModel — injected automatically by Compose’s hiltViewModel()
  • No manual factory classes needed

11. Push Notifications — FCM

Android implementation:
  • FirebaseMessagingService subclass — override onMessageReceived, onNewToken
  • onNewToken(token) → POST /user-devices (update FCM token)
  • Foreground: suppress OS notification, show custom Snackbar or banner Composable
  • Background/killed: OS shows notification; tap creates PendingIntent with data URL → handled in MainActivity.onNewIntent
  • Notification channels: APPOINTMENTS, MESSAGES, PAYMENTS — user can disable per-channel in OS settings
  • Android 13+ permission: POST_NOTIFICATIONS runtime permission requested after PIN screen

12. Offline Detection & UX

ConnectivityManager + NetworkCallback wrapped in a Flow:
  • NetworkMonitor singleton emits Flow<Boolean>collectAsStateWithLifecycle() in root Composable
  • Persistent Snackbar (non-dismissible): “Offline — showing cached data”
  • Mutations queued: needsSync = true in Room → SyncWorker picks up on reconnect
  • Optimistic UI: booking immediately reflected in local Room; server confirmation async

13. Roles & Screen Map

Patient

ScreenNav3 KeyNotes
HomeHomeKeyNext appointment card, quick-book FAB
AppointmentsAppointmentsKeyUpcoming + Past LazyColumn
Book appointmentBookAppointmentKeyModalBottomSheet — date → slot → confirm
Appointment detailAppointmentDetailKey(id)Cancel button if > 48h
RecordsRecordsKeyLazyColumn by type
Treatment planTreatmentPlanKey(id)Read-only
Prescription detailPrescriptionKey(id)Read-only
File viewerFileViewerKey(url, type)PdfRenderer API (built-in Android) or WebView fallback
BillsBillsKeyInvoice list
Invoice detailInvoiceKey(id)PDF inline
Chat inboxChatKeyClinic threads
Chat threadChatThreadKey(id)Material 3 bubbles
Profile + securityProfileKeyBiometric toggle, PIN change
NotificationsNotificationsKeyList with unread dots, swipe-to-mark-read

Doctor

ScreenNotes
Home (today’s schedule)Filtered by doctorId, LazyColumn timeline
Appointments + detail + actionsMark in-progress → completed; quick note
Patient searchSearchBar (Material 3), debounced
Patient profileTabRow: Vitals / Notes / Prescriptions / Files
Add clinical noteTextField, voice input via SpeechRecognizer
New prescriptionDrug search + dosage form

Admin

All doctor screens plus:
ScreenNotes
Stats dashboardElevatedCard KPI strip, LineChart (Vico library)
Finance — invoicesFull clinic list + FloatingActionButton
Create invoiceMulti-line LazyColumn items
Record paymentAmount + method DropdownMenu
Mobile permissions (read-only)Current clinic mobile access view

Receptionist

ScreenNotes
Home (check-in queue)Today sorted by time, CheckIn OutlinedButton per row
Appointments + New appointment
Patient search + New patient
Finance (invoices + payments)

14. UI Design System

Follows Material Design 3 Expressive (Google I/O 2025).
ElementImplementation
ColorMaterialTheme.colorScheme from dynamicColorScheme(context) (Android 12+); fallback lightColorScheme(primary = Color(0xFF5048E5))
TypographyMaterialTheme.typographydisplayLargelabelSmall; all scale with system font size
IconsMaterial Symbols (rounded variant); IconButton wrapping
Spacing8-dp grid; Arrangement.spacedBy(8.dp), .padding(16.dp)
Touch targetsMinimum 48×48 dp per Material 3 guidelines
ListsLazyColumn with ListItem composable; SwipeToDismissBox for actions
ButtonsButton (primary), OutlinedButton (secondary), TextButton (tertiary)
LoadingCircularProgressIndicator for full-screen; LinearProgressIndicator for inline
SkeletonShimmer effect via custom Modifier.shimmer() (Compose animation)
ChartsVico library (Jetpack Compose native charts) — no XML-based MPAndroidChart
SheetsModalBottomSheet (Material 3) for booking, filters
DialogsAlertDialog composable — no AlertDialog.Builder
Accessibilitysemantics { contentDescription = "…" }, Role.Button, clearAndSetSemantics

15. Android Studio + Gemini Integration

Android Studio Narwhal (2025.3, current May 2026) bundles Gemini for AI-assisted development:
Gemini featureHow to use in OdontoX
Inline code completionAuto-suggests Composable structure, ViewModel boilerplate, Hilt annotations
Generate Composable previewsRight-click → “Generate Preview” for any @Composable — instant @Preview annotation
Refactor with AISelect code → “Gemini: Refactor” → convert XML layouts to Compose, or improve state handling
Generate testsSelect ViewModel → “Gemini: Generate unit tests” → produces Hilt test + coroutine test scaffolding
Explain code”Gemini: Explain” on complex Room queries or Flow chains
Studio BotAsk “How do I implement biometric with CryptoObject?” — answers with project-aware context
Crash insightsLink Android Vitals → Gemini explains crash stack traces and suggests fixes
Usage rule: Gemini suggestions require human review before commit. Never accept Gemini output for auth/crypto code without manual security review. Use Gemini for boilerplate (DAOs, DTOs, ViewModels); write security code by hand.

16. Desktop-Only Features (not in Android app)

Same as iOS — identical feature parity decisions:
FeatureReason
Dental chart write mode6” screen unusable for tooth selection
Treatment plan authoringMulti-service tables require large viewport
DICOM workstationPixel-level zoom = desktop
Staff/user managementSecurity-sensitive
Clinic settingsPlan config, operating hours
Report builderFilter/export UI = desktop
PayrollDesktop

17. Security Checklist

  • EncryptedSharedPreferences for all tokens (AES-256-GCM key in Android Keystore)
  • network-security-config.xml: cleartextTrafficPermitted="false", pin api.odontox.io cert
  • FLAG_SECURE in MainActivity.onCreate() — blocks screenshots and recent apps thumbnail
  • BiometricPrompt with BIOMETRIC_STRONG + setAllowedAuthenticators(BIOMETRIC_STRONG)
  • ProGuard / R8 full mode enabled in release build — obfuscate class names
  • No PHI in LogcatLog.* calls filtered by BuildConfig.DEBUG flag
  • Root detection via RootBeer — log to backend, display warning
  • mPIN brute-force: 5 failures → logout + wipe EncryptedSharedPreferences
  • Certificate pinning via OkHttp.CertificatePinner
  • android:allowBackup="false" in Manifest — prevents device backup of app data
  • SQLCipher or EncryptedSharedPreferences for any local PHI fields (not Room without encryption)

18. Dependencies (version catalog — libs.versions.toml)

[versions]
kotlin = "2.1.0"
compose-bom = "2026.04.01"
hilt = "2.52"
retrofit = "2.11.0"
okhttp = "4.12.0"
room = "2.7.1"
datastore = "1.1.1"
navigation3 = "1.1.1"
biometric = "1.2.0"
work = "2.10.0"
vico = "2.1.2"
firebase-bom = "33.7.0"

[libraries]
# Compose BOM
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive" }
compose-navigation-suite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" }

# Navigation 3
navigation3-compose = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "navigation3" }

# Hilt
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version = "1.2.0" }

# Networking
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }

# Room
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }

# DataStore
datastore-proto = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }

# Auth
biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" }
security-crypto = { group = "androidx.security", name = "security-crypto", version = "1.1.0-alpha06" }

# WorkManager
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
hilt-work = { group = "androidx.hilt", name = "hilt-work", version = "1.2.0" }

# Charts
vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" }

# Firebase
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" }
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" }

19. Build & Distribution

StageTool
DevelopmentAndroid Studio Narwhal (2025.3, latest stable as of May 2026); emulator API 36 + physical device
Code signingRelease keystore in CI secrets (never committed)
CIGitHub Actions → ./gradlew assembleRelease + instrumented tests on Firebase Test Lab
Internal testingUpload .aab to Play Console → Internal Testing track (instant, no review)
ProductionPlay Console → Production track (< 3-7 h review for updates)
Minimum SDKAPI 28 (Android 9 Pie) — covers 97%+ of active devices as of 2026
Target SDKAPI 36 (Android 16, released 2026)
VersionversionCode auto-incremented by CI; versionName MAJOR.MINOR.PATCH

20. Mermaid Summary — Full Request Lifecycle