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
| Criterion | Reason |
|---|---|
| Material 3 Expressive | Google’s latest design language (2026, announced at Google I/O 2025) — dynamic color, adaptive layouts, research-backed motion — first-class in Compose |
| Performance | No JS bridge; Compose compiler generates optimized bytecode; skippable composables |
| Security | EncryptedSharedPreferences, Android Keystore, BiometricPrompt — direct framework APIs |
| PHI sensitivity | Network Security Config enforced at OS level; no third-party runtime |
| Tablet / foldable | NavigationSuiteScaffold + ListDetailPaneScaffold — adaptive layouts out-of-the-box |
| Android Studio + Gemini | Gemini-assisted code completion, refactoring, and test generation natively in IDE |
2. System Architecture
Layer responsibilities
| Layer | Owns | Never does |
|---|---|---|
| Composable | UI state → pixels; user events → ViewModel | Business logic, coroutine launch |
| ViewModel | Hold StateFlow<UiState>, map domain → UI model, handle events | Direct network/DB access |
| Use Case | One business operation as a suspend fun | UI concerns, DI |
| Repository | SSOT decision (local vs. remote), cache coordination | Business rules |
| Remote Source | Retrofit calls, OkHttp interceptors, DTO decoding | Caching, mapping |
| Local Source | Room DAOs, DataStore, EncryptedSharedPreferences | Sync 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
UiStateis a sealed class:Loading,Success(data),Error(message)
4. Project Structure
5. Navigation Architecture
Google’s official recommendation for Compose navigation in 2026: Navigation 3 (stable1.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 NavigationSuiteScaffoldauto-switches between BottomBar / NavigationRail / Drawer based onWindowSizeClass- Navigation 3’s
NavDisplaywith typed back-stack keys — no string-based routes ListDetailPaneScaffoldfor appointments list + detail on tabletsFLAG_SECUREset inMainActivity.onCreate()— persists for all screens
6. Auth Flow
Security rules (Google best practices):| Concern | Implementation |
|---|---|
| Token storage | EncryptedSharedPreferences (AES-256-GCM key in Android Keystore) — never plain SharedPreferences |
| mPIN storage | SHA-256(pin + UUID deviceSalt) in EncryptedSharedPreferences |
| Biometric | BiometricPrompt with BIOMETRIC_STRONG requirement — fallback to PIN, not device lock |
| Session lock | ProcessLifecycleOwner.lifecycle ON_STOP → start 15-min countdown coroutine; ON_START → check elapsed |
| 5 PIN failures | Force logout, wipe EncryptedSharedPreferences and Room DB |
| Network Security Config | network-security-config.xml — pins api.odontox.io cert, blocks cleartext |
| Screenshot | WindowManager.LayoutParams.FLAG_SECURE in MainActivity.onCreate() |
| Root detection | RootBeer 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 newFlow 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)
CacheEntrytable: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)
SyncWorkerwithConstraints(requiresNetwork = true)- Differential sync: include
updatedSincetimestamp 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 type | LruCache | Room | PHI stored |
|---|---|---|---|
| Appointment list | 5 min | 2 min | No |
| Patient profile | 30 min | 30 min | No (name/DOB excluded from Room) |
| Clinical notes | 5 min | Never | Memory only |
| Prescriptions | 5 min | Never | Memory only |
| Invoice list | 10 min | 10 min | No |
| PDF signed URLs | 4 min | Never | Memory only |
| Clinic info / slots | 24 h | 24 h | No |
| Notifications | On receive | Permanent | No |
Stale-While-Revalidate
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 WiFiconnectTimeout(30, SECONDS),readTimeout(30, SECONDS)HttpLoggingInterceptor(Level.NONE)in production,Level.BODYin debug only
- Retrofit with
GsonConverterFactory(or Moshi for faster parsing) @Streamingon 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: booltheme_mode: ThemeMode (SYSTEM / LIGHT / DARK)notification_opt_ins: repeated stringlast_sync_timestamp: int64
10. Dependency Injection — Hilt
Google’s standard DI framework for Android (replaces manual DI, Dagger boilerplate).@Singletonscope for Retrofit, OkHttpClient, Room, TokenManager@ViewModelScopedfor Use Cases (one instance per ViewModel)- All ViewModels annotated
@HiltViewModel— injected automatically by Compose’shiltViewModel() - No manual factory classes needed
11. Push Notifications — FCM
Android implementation:FirebaseMessagingServicesubclass — overrideonMessageReceived,onNewTokenonNewToken(token)→ POST/user-devices(update FCM token)- Foreground: suppress OS notification, show custom
Snackbaror banner Composable - Background/killed: OS shows notification; tap creates
PendingIntentwith data URL → handled inMainActivity.onNewIntent - Notification channels:
APPOINTMENTS,MESSAGES,PAYMENTS— user can disable per-channel in OS settings - Android 13+ permission:
POST_NOTIFICATIONSruntime permission requested after PIN screen
12. Offline Detection & UX
ConnectivityManager + NetworkCallback wrapped in a Flow:
NetworkMonitorsingleton emitsFlow<Boolean>—collectAsStateWithLifecycle()in root Composable- Persistent
Snackbar(non-dismissible): “Offline — showing cached data” - Mutations queued:
needsSync = truein Room →SyncWorkerpicks up on reconnect - Optimistic UI: booking immediately reflected in local Room; server confirmation async
13. Roles & Screen Map
Patient
| Screen | Nav3 Key | Notes |
|---|---|---|
| Home | HomeKey | Next appointment card, quick-book FAB |
| Appointments | AppointmentsKey | Upcoming + Past LazyColumn |
| Book appointment | BookAppointmentKey | ModalBottomSheet — date → slot → confirm |
| Appointment detail | AppointmentDetailKey(id) | Cancel button if > 48h |
| Records | RecordsKey | LazyColumn by type |
| Treatment plan | TreatmentPlanKey(id) | Read-only |
| Prescription detail | PrescriptionKey(id) | Read-only |
| File viewer | FileViewerKey(url, type) | PdfRenderer API (built-in Android) or WebView fallback |
| Bills | BillsKey | Invoice list |
| Invoice detail | InvoiceKey(id) | PDF inline |
| Chat inbox | ChatKey | Clinic threads |
| Chat thread | ChatThreadKey(id) | Material 3 bubbles |
| Profile + security | ProfileKey | Biometric toggle, PIN change |
| Notifications | NotificationsKey | List with unread dots, swipe-to-mark-read |
Doctor
| Screen | Notes |
|---|---|
| Home (today’s schedule) | Filtered by doctorId, LazyColumn timeline |
| Appointments + detail + actions | Mark in-progress → completed; quick note |
| Patient search | SearchBar (Material 3), debounced |
| Patient profile | TabRow: Vitals / Notes / Prescriptions / Files |
| Add clinical note | TextField, voice input via SpeechRecognizer |
| New prescription | Drug search + dosage form |
Admin
All doctor screens plus:| Screen | Notes |
|---|---|
| Stats dashboard | ElevatedCard KPI strip, LineChart (Vico library) |
| Finance — invoices | Full clinic list + FloatingActionButton |
| Create invoice | Multi-line LazyColumn items |
| Record payment | Amount + method DropdownMenu |
| Mobile permissions (read-only) | Current clinic mobile access view |
Receptionist
| Screen | Notes |
|---|---|
| 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).| Element | Implementation |
|---|---|
| Color | MaterialTheme.colorScheme from dynamicColorScheme(context) (Android 12+); fallback lightColorScheme(primary = Color(0xFF5048E5)) |
| Typography | MaterialTheme.typography — displayLarge → labelSmall; all scale with system font size |
| Icons | Material Symbols (rounded variant); IconButton wrapping |
| Spacing | 8-dp grid; Arrangement.spacedBy(8.dp), .padding(16.dp) |
| Touch targets | Minimum 48×48 dp per Material 3 guidelines |
| Lists | LazyColumn with ListItem composable; SwipeToDismissBox for actions |
| Buttons | Button (primary), OutlinedButton (secondary), TextButton (tertiary) |
| Loading | CircularProgressIndicator for full-screen; LinearProgressIndicator for inline |
| Skeleton | Shimmer effect via custom Modifier.shimmer() (Compose animation) |
| Charts | Vico library (Jetpack Compose native charts) — no XML-based MPAndroidChart |
| Sheets | ModalBottomSheet (Material 3) for booking, filters |
| Dialogs | AlertDialog composable — no AlertDialog.Builder |
| Accessibility | semantics { contentDescription = "…" }, Role.Button, clearAndSetSemantics |
15. Android Studio + Gemini Integration
Android Studio Narwhal (2025.3, current May 2026) bundles Gemini for AI-assisted development:| Gemini feature | How to use in OdontoX |
|---|---|
| Inline code completion | Auto-suggests Composable structure, ViewModel boilerplate, Hilt annotations |
| Generate Composable previews | Right-click → “Generate Preview” for any @Composable — instant @Preview annotation |
| Refactor with AI | Select code → “Gemini: Refactor” → convert XML layouts to Compose, or improve state handling |
| Generate tests | Select ViewModel → “Gemini: Generate unit tests” → produces Hilt test + coroutine test scaffolding |
| Explain code | ”Gemini: Explain” on complex Room queries or Flow chains |
| Studio Bot | Ask “How do I implement biometric with CryptoObject?” — answers with project-aware context |
| Crash insights | Link 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:| Feature | Reason |
|---|---|
| Dental chart write mode | 6” screen unusable for tooth selection |
| Treatment plan authoring | Multi-service tables require large viewport |
| DICOM workstation | Pixel-level zoom = desktop |
| Staff/user management | Security-sensitive |
| Clinic settings | Plan config, operating hours |
| Report builder | Filter/export UI = desktop |
| Payroll | Desktop |
17. Security Checklist
-
EncryptedSharedPreferencesfor all tokens (AES-256-GCM key in Android Keystore) -
network-security-config.xml:cleartextTrafficPermitted="false", pinapi.odontox.iocert -
FLAG_SECUREinMainActivity.onCreate()— blocks screenshots and recent apps thumbnail -
BiometricPromptwithBIOMETRIC_STRONG+setAllowedAuthenticators(BIOMETRIC_STRONG) -
ProGuard/ R8 full mode enabled in release build — obfuscate class names - No PHI in
Logcat—Log.*calls filtered byBuildConfig.DEBUGflag - 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)
19. Build & Distribution
| Stage | Tool |
|---|---|
| Development | Android Studio Narwhal (2025.3, latest stable as of May 2026); emulator API 36 + physical device |
| Code signing | Release keystore in CI secrets (never committed) |
| CI | GitHub Actions → ./gradlew assembleRelease + instrumented tests on Firebase Test Lab |
| Internal testing | Upload .aab to Play Console → Internal Testing track (instant, no review) |
| Production | Play Console → Production track (< 3-7 h review for updates) |
| Minimum SDK | API 28 (Android 9 Pie) — covers 97%+ of active devices as of 2026 |
| Target SDK | API 36 (Android 16, released 2026) |
| Version | versionCode auto-incremented by CI; versionName MAJOR.MINOR.PATCH |

