Native Mobile · Plan 2 of 2 · Android

MOS Field — Native Android App Plan

The Android twin of the iOS app plan: a comprehensive development plan for the native Android companion to the MOS Operations Platform, covering every current and future platform feature (Phases 1–5). Governed end-to-end by the Android App Constitution: Kotlin + Jetpack Compose, strict MVVM with StateFlow<UiState>, SOLID, Material Design 3, Android/Jetpack-native only — no third-party dependencies.

Target · minSdk 29 / targetSdk latest Stack · Kotlin / Compose / Room Architecture · MVVM + SOLID Dependencies · Android SDK + Jetpack only Backend · FastAPI + Supabase Status · Planning
01

Purpose & parity with iOS

MOS Field for Android is the same product as the iOS app — same roles, same phases, same backend contract — expressed natively in Kotlin, Jetpack Compose, and Material Design 3. It does not chase pixel-parity with iOS; it chases behavior parity: a field crew member on a Pixel and one on an iPhone can run the same day the same way. Everything platform-shaped (navigation, theming, storage, background work) follows Android idiom.

Feature parity Rule

iOS Phases A–D and Android Phases A–D carry identical feature scope. Neither platform ships a phase feature the other can't; the shared FastAPI contract enforces this naturally.

Platform idiom Rule

Material 3 components, dynamic color, predictive back gesture, Snackbars (never Toasts), 48dp touch targets, TalkBack — the app must feel like it was born on Android, not ported.

Field-device reality Android edge

Crews often carry rugged or mid-range Android hardware. minSdk 29 (Android 10) covers the realistic fleet; R8-shrunk builds and Baseline Profiles keep cold starts fast on weak devices.

Tablets Android edge

WindowSizeClass-adaptive layouts from day one — a truck-mounted tablet gets a two-pane jobs/detail layout for free instead of a stretched phone UI.

One backend, three clients. Web (Next.js), iOS, and Android all speak to the same FastAPI API with the same Supabase-issued ES256 JWT and the same server-side RBAC. The domain model, status flow (Assigned → In Progress → Waiting → Completed), and endpoint catalog are shared — defined once in the FastAPI OpenAPI schema.
02

Constitution compliance map

Every pillar of the Android constitution, mapped to the concrete decision in this plan. As on iOS, exactly one deviation exists and it is explicitly authorized by the product owner — the same deviation, for the same reason.

Constitutional pillarDecision in this planStatus
Stack — Kotlin, Compose, Single ActivityKotlin (latest stable), Jetpack Compose UI, one MainActivity, all navigation via Navigation Compose in a single NavHost (AppNavigation.kt). minSdk 29, targetSdk latest.Compliant
Dependency rule — no third-party librariesAllowed surface = Android SDK + AndroidX/Jetpack + kotlinx, exactly the libraries the constitution itself mandates (Navigation Compose, Room, androidx.biometric, androidx.security EncryptedSharedPreferences, kotlinx.serialization, DataStore, WorkManager, Glance). Forbidden = everything else: no Retrofit/OkHttp, no Hilt/Koin, no Glide/Coil, no Supabase Kotlin SDK. Networking is hand-rolled on HttpsURLConnection.Compliant
MVVM — UI / ViewModel / Repository / ModelOne ViewModel per screen exposing a single immutable StateFlow<FeatureUiState>; SharedFlow for one-shot events; Composables are logic-free and collect via collectAsStateWithLifecycle(); all I/O behind interface-typed Repositories (§03).Compliant
SOLIDInterface-first (AuthRepositoryInterface, NetworkServiceInterface, JobReadable/JobWritable…), constructor injection via a hand-written AppContainer (no Hilt — manual DI is the no-dependency answer), small role-scoped interfaces (§03).Compliant
Authentication — Google Sign-In primaryDeviation (owner-authorized, identical to iOS): primary auth is email/password against the existing Supabase employee accounts, because roles are admin-assigned to company identities and must match web + iOS. Biometric re-entry via androidx.biometric is kept exactly as mandated. Google Sign-In via Credential Manager can be layered on later as a linked provider.Authorized deviation
Encrypted storage onlyAccess + refresh tokens in EncryptedSharedPreferences (MasterKey, AES256_GCM) behind a typed EncryptedPreferencesManager. Nothing sensitive in plain SharedPreferences, DataStore, or rememberSaveable.Compliant
Networking — HttpsURLConnection, typedInterface-driven layer returning NetworkResult<T>; kotlinx.serialization with explicit @SerialName; cert pinning via network_security_config.xml; usesCleartextTraffic="false"; all calls suspend on Dispatchers.IO (§05).Compliant
Persistence — Room + encrypted prefs + DataStoreRoom @Entity/@Dao/single @Database behind repositories; DAOs are suspend or return Flow; offline write-queue drained by WorkManager; DataStore for benign preferences (§06).Compliant
Material Design 3M3 components exclusively; Theme.kt/Color.kt/Typography.kt/Shape.kt; dynamic color on Android 12+ with a static MOS brand scheme as fallback; light + dark tested; sp-scaled type; 48dp targets; TalkBack + WCAG 2.1 AA contrast (§07).Compliant
Coroutinessuspend/Flow everywhere; viewModelScope only; Dispatchers.IO for I/O; no GlobalScope, no callbacks in new code.Compliant
Release hardeningFLAG_SECURE in release, allowBackup="false", R8 full mode with keep rules for serialization models + Keystore classes, PII-stripping Logger behind BuildConfig.DEBUG (§09).Compliant
Testing — JUnit + coroutines-test, no mocking frameworksInterface-based fakes for every injected dependency; >80% business-logic coverage; Compose UI tests for sign-in, jobs, photo flows; deterministic dispatchers (§10).Compliant
The single deviation, on the record. Constitution §6 names Google Sign-In (Credential Manager) as the primary auth method. For an internal operations tool where the office assigns roles to company Supabase accounts, Google-account-first would break identity parity with web and iOS. The product owner has directed email/password as primary on both mobile platforms. Everything else in §6 — encrypted storage, biometrics, cleartext ban, pinning, FLAG_SECURE, R8 — applies unchanged.
Push notifications need one sanctioned Google service. There is no Android-native push channel other than Firebase Cloud Messaging — the constitution's companion PRD explicitly names FCM, so firebase-messaging (Google-published) is treated as sanctioned platform infrastructure, not a third-party dependency. It is the only such addition, and it is isolated behind PushRegistrationService so the rest of the app never sees Firebase types.
03

Architecture & project layout

Strict MVVM with a core/repository spine, instantiated with MOS feature names under com.mikeodellsurveys.mosfield. Phase A packages are created at kickoff; later-phase packages only when their phase begins (no speculative scaffolding, constitution §4).

/app/src/main/java/com/mikeodellsurveys/mosfield/
  /app
    MainActivity.kt              ← single Activity; FLAG_SECURE in release
    AppNavigation.kt             ← one NavHost; routes as a sealed class
    AppContainer.kt              ← manual constructor-injection graph (no Hilt)
  /features
    /auth          /ui /viewmodel /model
    /jobs          /ui /viewmodel /model    ← list, detail, status updates
    /notes         /ui /viewmodel /model
    /documents     /ui /viewmodel /model
    /photos        /ui /viewmodel /model    ← Phase A
    /fieldreports  /ui /viewmodel /model    ← Phase A
    /timetracking  /ui /viewmodel /model    ← Phase B
    /mileage       /ui /viewmodel /model    ← Phase B
    /equipment     /ui /viewmodel /model    ← Phase B
    /dashboard     /ui /viewmodel /model    ← Phase C (owner/PM)
    /search        /ui /viewmodel /model    ← Phase C
    /voicereports  /ui /viewmodel /model    ← Phase D
  /core
    /network       NetworkService.kt · ApiEndpoints.kt · NetworkResult.kt · CertPinnedConnectionFactory.kt
    /repository    JobRepository.kt · NoteRepository.kt · DocumentRepository.kt
                   TimeEntryRepository.kt · UploadQueueRepository.kt
    /database      AppDatabase.kt · JobDao.kt · NoteDao.kt · UploadQueueDao.kt
    /security      KeystoreManager.kt · BiometricManager.kt · EncryptedPreferencesManager.kt
    /auth          AuthManager.kt           ← GoTrue REST client + token refresh mutex
    /sync          SyncWorker.kt · PhotoUploadWorker.kt   ← WorkManager
  /shared
    /components    PrimaryButton.kt · InputField.kt · LoadingIndicator.kt
                   StatusPill.kt · RoleBadge.kt · DataStateContent.kt
    /extensions    ContextExtensions.kt · StringExtensions.kt
    /constants     AppConstants.kt · ApiConstants.kt
    /utils         Logger.kt · Validator.kt
  /theme
    Theme.kt · Color.kt · Typography.kt · Shape.kt
Design-language parity. StatusPill, RoleBadge, and DataStateContent (loading/empty/error/populated) mirror the primitives shipped in apps/web and planned for iOS. MOS brand colors live in Color.kt feeding the static M3 color scheme; dynamic color is offered on Android 12+ per the constitution, with the brand scheme as the out-of-box default so the app still reads as MOS.

Layer contract

LayerRules applied
Model@Serializable data classes with explicit @SerialName mirroring API schemas (Job, JobNote, UserProfile, RoleKey enum). No Compose imports. Domain rules (allowed status transitions) as pure functions on the model.
ViewModelOne per screen; private MutableStateFlow, public immutable StateFlow<UiState>; SharedFlow for navigation/snackbar events; depends only on interfaces via constructor; launches in viewModelScope; never touches HttpsURLConnection or Room directly.
UI (Composables)Scaffold-rooted screens; collectAsStateWithLifecycle(); zero business logic; errors via M3 Snackbar; every interactive element has contentDescription/semantics and a 48dp minimum target.
Repository / ServiceOne data domain each; interfaces split by consumer need (JobReadable vs JobWritable); combine Room + network; expose Flow for streams, suspend for one-shots.
// The UiState + DI pattern used by every screen in the app
data class JobListUiState(
    val isLoading: Boolean = false,
    val jobs: List<Job> = emptyList(),
    val pendingSyncCount: Int = 0,
    val errorMessage: String? = null
)

class JobListViewModel(
    private val jobRepository: JobReadable
) : ViewModel() {
    private val _uiState = MutableStateFlow(JobListUiState())
    val uiState: StateFlow<JobListUiState> = _uiState.asStateFlow()

    fun loadAssignedJobs() = viewModelScope.launch {
        _uiState.update { it.copy(isLoading = true) }
        when (val result = jobRepository.assignedJobs()) {
            is NetworkResult.Success -> _uiState.update { it.copy(isLoading = false, jobs = result.data) }
            is NetworkResult.Error   -> _uiState.update { it.copy(isLoading = false, errorMessage = result.error.toUserMessage()) }
        }
    }
}
04

Authentication & roles

Identical auth contract to iOS: Supabase Auth (GoTrue) consumed as plain HTTPS REST — no Supabase Kotlin SDK — so the whole stack is HttpsURLConnection + EncryptedSharedPreferences + BiometricPrompt, all inside the constitution's allowed surface.

Token lifecycle

StepMechanism
1 · Sign inPOST /auth/v1/token?grant_type=password on the Supabase project URL with the publishable (anon) key header. Response decoded via kotlinx.serialization into a typed SessionTokens model.
2 · StoreAccess + refresh tokens → EncryptedPreferencesManager (MasterKey AES256_GCM, Android Keystore-backed). The anon key is a publishable client identifier; real secrets never ship in the APK.
3 · Call the APIEvery FastAPI request carries Authorization: Bearer <access_token>; the backend verifies the ES256 signature via JWKS — no shared secret on any client.
4 · RefreshAuthManager refreshes proactively before expiry and reactively on 401, serialized behind a Mutex so concurrent requests trigger exactly one refresh.
5 · Re-entryOn cold start/foreground with a stored session: BiometricPrompt (class 3 biometrics, device-credential fallback) gates access before tokens are read. Sign-out calls clearToken() and wipes the Room cache.
6 · RolesGET /api/me + GET /api/me/roles (already live) hydrate UserProfile and List<RoleKey>; roles select the NavigationBar destinations. Client checks are UX only — FastAPI re-enforces RBAC from the database on every request.

Role-aware navigation (single NavHost)

Future Google Sign-In. Supabase supports Google as an OAuth provider, so restoring full §6 compliance later is additive: a Credential Manager flow feeding the same GoTrue token exchange and an account-linking step — token storage, refresh, and RBAC are untouched.
05

Networking layer

One interface-driven layer on HttpsURLConnection, exactly per constitution §7 — the constitution's own contracts, extended with the MOS endpoint catalog and error mapping.

interface NetworkServiceInterface {
    suspend fun <T> request(endpoint: Endpoint, deserializer: DeserializationStrategy<T>): NetworkResult<T>
}

data class Endpoint(
    val path: String,
    val method: HttpMethod,                 // GET · POST · PUT · DELETE · PATCH
    val headers: Map<String, String> = emptyMap(),
    val body: String? = null
)

sealed class NetworkResult<out T> {
    data class Success<T>(val data: T) : NetworkResult<T>()
    data class Error(val error: AppError) : NetworkResult<Nothing>()
}

sealed class AppError {
    object NetworkUnavailable : AppError()
    object Unauthorized : AppError()
    object Forbidden : AppError()          // role gate — server said no
    object DecodingFailed : AppError()
    data class ValidationFailed(val message: String) : AppError()
    data class HttpError(val code: Int, val message: String) : AppError()
    object SyncConflict : AppError()
}
06

Persistence & offline-first

Same product decision as iOS — the local store is the read source of truth and writes queue when offline — implemented the Android way: Room for domain data, WorkManager for guaranteed background sync.

Room entities (repositories only — never touched from UI or ViewModel)

@EntityPurposeSync behavior
JobEntityAssigned jobs cache: number, client, site, status, due, crewPull-refresh + on-launch delta fetch; server wins on conflict; DAO exposes Flow<List<JobEntity>>
JobNoteEntityNotes authored on deviceQueued write; pending → synced flag
PhotoUploadEntityCaptured photos + job link + GPS + timestampPhotoUploadWorker (WorkManager, network-constrained, backoff) — survives process death and reboot
DailyReportEntityField report draftsDraft locally; explicit submit; queued if offline
TimeEntryEntityClock in/out pairs (Phase B)Timestamped at tap-time; reconciled on sync
MileageEntityTrip logs (Phase B)Queued write
07

Feature roadmap — Android Phases A–D

Identical feature scope to the iOS phases, with each feature naming its MVVM units and the Android/Jetpack-native API that satisfies the dependency rule. Later phases are planned here but scaffolded only when they begin.

Android Phase A — Field MVP

maps platform Phase 1 + Phase 2 · ships first

A field crew member can run their whole day from the phone: see jobs, get to site, capture evidence, file the daily report.

FeatureMVVM unitsAndroid APIs
Sign in + biometric re-entryLoginScreen/ViewModel, BiometricGateScreen, AuthManager, EncryptedPreferencesManagerandroidx.biometric, androidx.security, Android Keystore
My Jobs list + detailJobListScreen/ViewModel, JobDetailScreen/ViewModel, JobRepositoryCompose, Room, Navigation Compose
Status updates (Assigned → In Progress → Waiting → Completed)Transition rules in Job model; JobDetailViewModel.updateStatus()— (pure domain logic)
Job notesNoteComposer composable + NoteRepositoryCompose
Document viewingDocumentListScreen/ViewModel, DocumentRepositoryPdfRenderer, platform viewers via SAF
Photo capture + upload (GPS + timestamp stamped)PhotoCaptureScreen/ViewModel, UploadQueueRepository, PhotoUploadWorkerCameraX (Jetpack), WorkManager, location APIs
Site maps + navigation hand-offJobMapSection in job detail; deep-link out to the device's maps app for turn-by-turnLocation APIs + geo-intent hand-off (no bundled map SDK — keeps the dependency rule intact)
Daily field reportsFieldReportScreen/ViewModel, DailyReportEntityCompose, Room
Push notifications (assignment, status, due-date)PushRegistrationService; notification tap deep-links into job detail via NavHostFCM (sanctioned, isolated), UserNotifications-equivalent NotificationManager channels

Android Phase B — Time & assets

maps platform Phase 3

The phone becomes the timesheet and the equipment ledger.

FeatureMVVM unitsAndroid APIs
Clock in/out with arrive/leave reminders at the siteTimeClockScreen/ViewModel, TimeEntryRepository, LocationServiceGeofencing / location APIs, Room, WorkManager
Mileage tracking (start/stop trip, odometer entry)MileageScreen/ViewModel, MileageEntityLocation APIs
Equipment checkout/return (GPS units, total stations)EquipmentScreen/ViewModel, EquipmentRepositoryCompose; CameraX barcode capture if assets are tagged
Payroll-ready summaries (read-only week view)TimesheetSummaryScreen/ViewModelCompose

Android Phase C — Management & insight

maps platform Phase 4

Owner and office roles get real value beyond the web parity baseline.

FeatureMVVM unitsAndroid APIs
Executive dashboard (active jobs, revenue, utilization, AR)DashboardScreen/ViewModelCompose Canvas charts (no chart library exists in the allowed surface — bar/line/donut composables are built once in /shared/components)
Reports (rendered server-side, viewed natively)ReportListScreen/ViewModelPdfRenderer
Historical search across jobs/clients/docs/notesSearchScreen/ViewModel, debounced Flow query to backendCompose
Office scheduling adjustmentsScheduleScreen/ViewModelCompose; WindowSizeClass two-pane on tablets
Home-screen at-a-glance (jobs due today / crew status)Glance widgetandroidx.glance (Jetpack)

Android Phase D — Intelligence

maps platform Phase 5 · AI features surfaced natively

Heavy AI runs on the backend; the app contributes what only a phone can — voice, camera, and context.

FeatureMVVM unitsAndroid APIs
Voice-to-report — dictate the daily report hands-free; on-device transcription, then backend structuringVoiceReportScreen/ViewModel, SpeechTranscriptionServiceandroid.speech.SpeechRecognizer (on-device where supported)
Document summaries — AI summaries of title commitments/deeds on job detailSummary section in JobDetailViewModel— (backend-computed)
Intelligent search — natural-language queries against the backend AI searchExtends SearchViewModelCompose
Smart capture — document scanning with perspective correction for field paperworkDocScanScreenCameraX + backend correction; ML Kit doc scanner considered only if the owner sanctions it as Google-service infrastructure (like FCM)
08

Backend work the app depends on

Nearly identical to the iOS list — by design. The one Android-specific addition is FCM delivery alongside APNs in the push service.

Needed byBackend additionNotes
Phase AJobs CRUD + status transition endpoints (/api/jobs…)Day-5 web scope; shared as-is
Phase ANotes + documents endpoints; presigned upload/download URLs (Supabase Storage brokered by FastAPI)App never holds storage credentials — short-lived signed URLs only
Phase ADevice-token registration (POST /api/devices with a platform field) + push fan-out service speaking both APNs and FCMOne registration endpoint serves both mobile apps
Phase BTime-entry, mileage, and equipment endpointsSame tables feed payroll export on the web
Phase CDashboard aggregates + search endpointServer-computed; app only renders
Phase DAI endpoints: report structuring from transcript, doc summaries, NL searchPlatform Phase 5 scope
Contract-first, cross-platform. Every endpoint lands in the FastAPI OpenAPI schema first; Kotlin @Serializable models and Swift Codable structs are both written against that schema. One source of truth, three clients — divergence is impossible by construction.
09

Security checklist — constitution §6 applied

10

Testing, delivery & milestones

Testing standards (constitution §12)

Distribution & quality telemetry

Milestones

M0 · Kickoff — ~1 wk
Project foundation
Gradle project, package skeleton, M3 theme (brand scheme + dynamic color), EncryptedPreferencesManager, NetworkService + pinning config, AuthManager + biometric gate, AppNavigation + AppContainer. Unit tests from the first file.
M1 · ~2–3 wks
Jobs core
Job list/detail/status/notes/documents against the Day-5 Jobs API; Room cache with Flow-driven UI + pull-to-refresh; DataStateContent states everywhere.
M2 · ~2–3 wks
Field capture
CameraX photo capture + WorkManager upload queue, site location + maps hand-off, daily field reports, offline write-queue + SyncWorker.
M3 · Internal track
Phase A in crews' hands
FCM push live; Play internal testing rollout; Android Vitals baseline recorded. Exit bar: constitution §15 checklist green on every screen, release R8 build verified.
M4 · ~2 wks
Phase B — time & assets
Clock in/out + site reminders, mileage, equipment checkout, payroll-ready week view.
M5 · ~2 wks
Phase C — management
Owner dashboard (Canvas charts), historical search, schedule view with tablet two-pane, Glance widget.
M6 · scoped later
Phase D — intelligence
Voice-to-report, AI summaries, NL search, smart capture — sequenced with platform Phase 5 backend delivery.
Definition of done, every screen: the constitution §15 checklist — MVVM separation, SOLID, no third-party deps, encrypted-storage-only secrets, HTTPS + coroutines, typed AppError handling, contentDescription/semantics everywhere, light + dark tested, font scaling, no !!, ViewModel/Repository tests, strings.xml, no dead code, FLAG_SECURE in release, R8 rules verified.
Sequencing vs iOS. Both apps share the backend contract, so they can be built in either order or in parallel by separate tracks. If sequential, whichever ships second inherits the proven API shapes, push service, and product decisions from the first — plan the second at roughly 70–80% of the first's effort.
11

Appendix — the Constitution (verbatim)

Android App Constitution — the governing document for all Android work on this project, reproduced as supplied.

Android App Constitution — click to expand

Project structure

/app/src/main/java/com/[company]/[appname]/ — /app (MainActivity.kt — single Activity entry point; AppNavigation.kt — NavHost and route definitions) · /features/[featurename]/{ui: FeatureScreen.kt, FeatureComponents.kt · viewmodel: FeatureViewModel.kt, FeatureUiState.kt · model: FeatureModel.kt} · /core/{network: NetworkService.kt, ApiEndpoints.kt, NetworkResult.kt · repository: UserRepository.kt · database: AppDatabase.kt, UserDao.kt · security: KeystoreManager.kt, BiometricManager.kt, EncryptedPreferencesManager.kt · auth: AuthManager.kt} · /shared/{components: PrimaryButton.kt, InputField.kt, LoadingIndicator.kt · extensions: ContextExtensions.kt, StringExtensions.kt · constants: AppConstants.kt, ApiConstants.kt · utils: Logger.kt, Validator.kt} · /theme (Theme.kt, Color.kt, Typography.kt, Shape.kt)

3. SOLID Principles — Applied to Kotlin

  • S — Single Responsibility: Every class, object, and interface does one thing. A ViewModel manages state for one screen. A Repository handles one data domain. A UseCase encapsulates one business operation. If a file exceeds ~150 lines, question whether it has multiple responsibilities.
  • O — Open/Closed: Use interfaces and extension functions to extend behavior. Never modify existing working code to add new features — extend it. Prefer interface-based abstractions over concrete type dependencies.
  • L — Liskov Substitution: Any implementing class must be fully substitutable for its interface. Do not add TODO() or throw NotImplementedError() in production implementations.
  • I — Interface Segregation: Define small, focused interfaces. Never force a class to implement methods it does not need. Example: split UserRepositoryInterface into UserReadable, UserWritable, UserDeletable.
  • D — Dependency Inversion: ViewModels depend on interface abstractions, never concrete repository or service implementations. Inject dependencies via constructor injection. This enables testability and clean separation.

4. Simplicity — No Bloat

  • Write only what is needed. Do not scaffold features that have not been requested.
  • No over-engineering. If a simple data class solves the problem, do not create a class hierarchy.
  • No premature abstraction. Create an interface when you have two or more concrete implementations, not speculatively.
  • No redundant comments. Code should be self-documenting. Only comment on why, never what.
  • No dead code. Do not leave TODO stubs, unused variables, or commented-out blocks.
  • Prefer data classes and sealed classes over complex inheritance hierarchies.
  • Avoid massive files. Split large composables into focused sub-composables.
  • Single Activity architecture — one MainActivity, all navigation via Navigation Compose.

5. Material Design 3 & Android Design Guidelines

  • Layout & Navigation: Navigation Compose for all navigation. Single NavHost in AppNavigation.kt — no FragmentManager, no Intent-based navigation between screens. Scaffold as root composable. BottomNavigation for top-level navigation with 3–5 destinations. Respect window insets (WindowInsets, imePadding(), systemBarsPadding()). Support Dynamic Type via sp units.
  • Components: Material 3 components exclusively (Button, OutlinedTextField, Card, TopAppBar, NavigationBar, Switch, Slider, Checkbox). Never build custom versions of components M3 already provides. Use MaterialTheme.colorScheme / .typography / .shapes — never hardcode colors, font sizes, or corner radii inline.
  • Typography: All typography in Typography.kt using the M3 type scale. Never hardcode fontSize inline.
  • Color & Theming: All colors in Color.kt as named constants, applied through MaterialTheme.colorScheme. Light and Dark themes via dynamicColorScheme (Android 12+) with static fallback. Test all screens in both modes.
  • Accessibility: Every interactive composable has contentDescription or a semantics block. Minimum 48×48dp touch targets (Modifier.minimumInteractiveComponentSize()). Modifier.semantics for custom actions/state. TalkBack tested. WCAG 2.1 AA contrast (4.5:1 normal, 3:1 large).

6. Security — Best Practices (Android-Native Only)

  • Authentication: Google Sign-In (androidx.credentials via Credential Manager API) as the primary auth method. Biometric authentication via androidx.biometric for app re-entry. Never store passwords or tokens in SharedPreferences — EncryptedSharedPreferences or Android Keystore exclusively.
  • Keystore & Encrypted Storage: All sensitive data via EncryptedSharedPreferences backed by Android Keystore. MasterKey with AES256_GCM. All secure storage wrapped in a dedicated EncryptedPreferencesManager with typed methods (saveToken/getToken/clearToken, saveSecureValue/getSecureValue for SecureKey).
  • Networking: HttpsURLConnection only — reject HTTP. android:usesCleartextTraffic="false". Certificate pinning via network_security_config.xml for production endpoints. Validate all server responses with type-safe decoding. kotlinx.serialization with explicit @SerialName — never Any or untyped maps.
  • Data Protection: android:allowBackup="false" unless backup is explicitly required and encrypted. Never log sensitive data — custom Logger strips PII in release. BuildConfig.DEBUG guards on diagnostics. FLAG_SECURE on the MainActivity window in non-debug builds.
  • Input Validation: Validate and sanitize all user input before processing/transmitting. Dedicated Validator with typed rules. Reject over-length input before the ViewModel.
  • ProGuard / R8: R8 full mode in release. Explicit keep rules for serialization models and Keystore classes. Never disable minification/obfuscation in production.

7. Networking Layer

Only HttpsURLConnection from the Android SDK, structured through a typed, interface-driven service layer: NetworkServiceInterface.request(endpoint, deserializer): NetworkResult<T>; Endpoint(path, method, headers, body); HttpMethod { GET, POST, PUT, DELETE, PATCH }; NetworkResult = Success(data) | Error(AppError). All calls are suspend functions (viewModelScope, Dispatchers.IO). No callbacks in new code. All HTTP error codes mapped explicitly to typed AppError cases. Responses decoded into typed Kotlin models immediately — never raw String or JSONObject to the ViewModel. All network operations on Dispatchers.IO, never the main thread.

8. State Management

  • StateFlow<UiState> — primary mechanism for exposing screen state from ViewModel to UI
  • MutableStateFlow — private to ViewModel; only the immutable StateFlow is exposed
  • SharedFlow — one-time events (navigation, snackbars) not replayed on recomposition
  • rememberSaveable — local UI state that must survive configuration changes
  • remember — local UI state that does not need to survive configuration changes
  • Never store sensitive data in rememberSaveable or Bundle
  • Collect with collectAsStateWithLifecycle() — never lifecycle-unaware collectAsState()

9. Error Handling

Typed AppError sealed class (NetworkUnavailable, Unauthorized, DecodingFailed, ValidationFailed(message), HttpError(code, message)) with toUserMessage(). Every suspending function returns NetworkResult<T> or propagates typed exceptions — no empty catch blocks. ViewModels expose errorMessage within the UiState data class. Errors shown via Material 3 Snackbar (SnackbarHostState) — never Toast.

10. Kotlin Coroutines Rules

suspend functions and Flow for all async operations. Launch from viewModelScope — never from composables directly. Dispatchers.IO for network/database; Dispatchers.Default for CPU-intensive work. Never GlobalScope. withContext(Dispatchers.Main) only when explicitly needed. viewModelScope cancels automatically on onCleared(). Flow for streams (Room, continuous sources); suspend for one-shots.

11. Persistence

Room as primary local persistence. EncryptedSharedPreferences for all sensitive persistent data. Preferences DataStore for non-sensitive preferences — never SharedPreferences directly. Clear @Entity data classes, @Dao interfaces, a single @Database class. Never perform database operations in a ViewModel or Composable — route through the Repository layer. All DAO methods suspend or returning Flow<T>.

12. Testing Standards

Unit tests for every ViewModel and Repository (JUnit 4/5 + kotlinx-coroutines-test). Interface-based fakes/mocks — no mocking frameworks. >80% coverage on business logic. Compose UI tests (ui-test-junit4) for critical flows. No production code ships with failing tests. TestCoroutineDispatcher / UnconfinedTestDispatcher for deterministic coroutine testing.

13. Code Style & Conventions

Kotlin coding conventions — clear, expressive names. Default to private. data class for state/models, immutable via val. sealed class/interface for exhaustive state — always exhaustive when. No !! — use ?.let, ?: return, or requireNotNull() with a message. when over if-else chains. Extension functions for reusable utilities. object for singletons. Standard file header comment (filename, project, creation date) in every file.

14. What You Must Never Do

  • Import third-party libraries (Retrofit, Hilt, Glide, etc.) — violates no-external-dependency rule
  • Store tokens in SharedPreferences — use EncryptedSharedPreferences
  • Use HTTP endpoints — plaintext data transmission
  • Put business logic in Composables — violates MVVM
  • Use GlobalScope — unscoped, uncontrolled coroutine lifecycle
  • Use !! non-null assertion — runtime NPE risk
  • Hardcode API keys in source — security vulnerability
  • Use JSONObject / Any for parsing — type safety violation
  • Skip contentDescription on interactive elements — accessibility violation
  • Use Toast for user-facing error messages — use Snackbar
  • Use multiple Activities for navigation — violates Single Activity architecture
  • Leave TODO comments in delivered code — incomplete implementation
  • Enable android:allowBackup="true" without encryption — data exposure risk
  • Use android:usesCleartextTraffic="true" — plaintext network traffic

15. Checklist Before Delivering Any Feature

  • MVVM layers cleanly separated (UI / ViewModel / Repository / Model)
  • All SOLID principles applied
  • No third-party dependencies introduced
  • Sensitive data in EncryptedSharedPreferences / Keystore only
  • All network calls use HTTPS and Kotlin Coroutines (suspend)
  • Typed error handling via AppError sealed class
  • contentDescription / semantics on all interactive composables
  • Light Mode and Dark Mode both tested and supported
  • Dynamic Type (font scaling) supported — no hardcoded sp values
  • No !! non-null assertions in production code
  • Unit tests for ViewModel and Repository logic
  • No hardcoded strings — all user-facing strings in strings.xml
  • No dead code, stubs, or TODO comments
  • FLAG_SECURE applied in release builds
  • ProGuard/R8 rules verified for release build

This constitution governs all Android development in this project. When in doubt, choose the simpler, more secure, more native solution.