Compose vs Flutter

Build the new Urvi in Kotlin with Jetpack Compose, with domain, data and sync in plain Kotlin modules that can move to Kotlin Multiplatform later. Switch to Flutter only if an iOS field app is a firm requirement within about a year and the reviewers are comfortable reviewing Dart.

What this app actually needs

NeedEvidence in today's codeWhy it matters for the choice
Offline-first store with heavy writesAbout 50 entities, a sync ledger with dependencies, about 40 response handlersThe sync engine is the hardest, most valuable part
Reliable background sync23 WorkManager workers, foreground dataSync workers, OEM battery killers in rural AndroidNative WorkManager is first-class; cross-platform wraps it
Large forms with complex validationFarmer profile (21 fields, cascading rules), crop block (PMDS, A Grade, edit reasons), KAP (ceilings)Both frameworks handle this; rules must live in a testable domain layer
Maps and walking GPS polygon captureosmdroid, 5 m displacement, 2 s interval, KML filesNeeds a foreground location service during capture and offline tiles
Camera and signaturesSystem camera intent, compression, signature padEasy in both
Low-end Android, 2G/3GField phones, no R8 todayCold start, memory and APK size count
Telugu and Tamil, in-app language switchvalues-te, values-ta, LocaleManagerExisting strings.xml can be reused natively
In-place upgrade with unsynced dataPending rows in agri_db, Java-serialized ops in SYNC_QUEUE_DBThe new app must read the old store once to avoid data loss
Team skillsAgents write the code; 3 reviewers who read Kotlin fluently; 99.7% Kotlin today, no Dart in the repoReviewers must catch agent mistakes in sync and migration code, which is faster in a language they know
iOSNo iOS code or stated requirementThe main argument for Flutter is not proven yet

Scored comparison

Scores are 1 (poor) to 5 (strong) for this app, not in general. Weight is 1-5.

CriterionWeightComposeFlutterNote
Offline DB and sync engine553Room + WorkManager are native; Flutter needs drift plus a headless-isolate WorkManager plugin
Background work on OEM low-end Android553Expedited and foreground work, Doze handling, debugging are simpler natively
Reading the old on-device data at upgrade542Kotlin can read agri_db and, if needed, deserialize old queue ops; Dart can read SQLite but not Java-serialized params
Review confidence453Agents write both well; the 3 reviewers catch sync and migration mistakes faster in Kotlin than in Dart
Large forms and validation444Both fine; Flutter's form packages are mature, Compose needs a small in-house form-state helper
Maps and walking GPS capture443Fused location + foreground service is direct in Kotlin; Flutter needs geolocator plus a foreground-service plugin
Low-end performance and APK size443Flutter adds an engine (several MB) and slower cold start on old ARMv7 phones
Localization (te, ta)354Reuse strings.xml and the per-app language API vs convert to ARB
Reuse of existing assets341Assessment library AAR, validators and rules can be lifted into Kotlin; Flutter rewrites everything
Incremental delivery342Compose screens and the new outbox can be trialled inside the current app; Flutter is a big-bang or awkward add-to-app
iOS or web reach235Kotlin Multiplatform shares logic; UI on iOS needs Compose Multiplatform or SwiftUI
Camera and signature254CameraX or system intent vs camera / image_picker plugins
UI iteration speed245Hot reload is better in Flutter; Compose previews and Live Edit close some of the gap
Agent code quality255Claude writes idiomatic Kotlin/Compose and Dart/Flutter equally well
Weighted total (of 240)211 (88%)151 (63%)

If iOS becomes a must-have (weight 5 instead of 2), the totals move to 220 vs 166. Compose still leads on score, but the real cost difference then is building a second UI, which is why iOS is the main flip condition below.

Risks of each option

OptionRiskMitigation
ComposeCompose performance on 2-3 GB phones if R8 and Baseline Profiles are skippedMake R8, Baseline Profiles and a Macrobenchmark gate part of the day 9 hardening
ComposeAgents copy old patterns (fragments, Executors, !!) from today's codeArchitecture rules in CLAUDE.md, lint rules that fail the build, a review agent with a checklist
ComposeiOS arrives later and needs a second UIKeep domain, data and sync free of Android imports so they can move to commonMain
ComposeMap choice: osmdroid is ageingUse MapLibre Native (or keep osmdroid inside AndroidView for the day 5 land work only)
FlutterAgents write Dart well, but the 3 reviewers are slower to spot mistakes in Dart sync and migration codeOpus review agent on every sync PR; extra torture tests on day 2
FlutterBackground sync via a Dart isolate is less predictable on low-RAM OEM phonesKeep sync logic small; native Kotlin WorkManager shim calling Dart only for mapping
FlutterCannot read Java-serialized SYNC_QUEUE_DB opsMandatory drain release of the old app before any upgrade; server-side recovery of DB dumps
FlutterAssessment library must be rewrittenDecide early whether surveys stay; if yes, give it a full day of agent work
FlutterPlugin quality for location foreground service, tile cachingSpike both on day 1 on the slowest field phone

The Kotlin Multiplatform option

KMP is not a third UI framework. It lets the domain rules, data layer, App Service client and sync engine live in commonMain, compiled for Android now and iOS later.

LayerShared in KMPPlatform-specific
Domain rules (category gate, land usage, validators, season logic)Yes
App Service client (Ktor, kotlinx.serialization)YesEngine: OkHttp on Android, Darwin on iOS
Local DB (Room KMP or SQLDelight) and outboxYesDriver and SQLCipher setup
Sync engine (outbox drain, delta apply)YesScheduler: WorkManager on Android, BGTaskScheduler on iOS
UIOptional (Compose Multiplatform)Compose on Android; SwiftUI or Compose Multiplatform on iOS
Maps, camera, location, secure storagePlatform code behind small interfaces

Recommendation: structure the Compose app as if it were KMP (pure Kotlin domain, data and sync modules with no Android imports, Ktor and kotlinx.serialization rather than Gson) but ship Android-only until iOS is confirmed. The extra cost is small now and avoids a rewrite later.

Recommendation

Choose Jetpack Compose, KMP-ready. The parts that decide Urvi's success are offline storage, the outbox, background sync on cheap Android phones and a safe upgrade for devices holding unsynced farmer data. Native Kotlin is strongest exactly there, the reviewers already read Kotlin, and the existing rules can be extracted and unit-tested rather than translated.

Flip to Flutter if all of these hold:

ConditionWhy it flips the call
An iOS (or web) field app is a firm requirement within about 12 monthsOne UI codebase beats two
The reviewers are comfortable reviewing Dart, or one Flutter-experienced reviewer joinsRemoves the review risk on the sync engine
The programme accepts a mandatory drain release and a short field freeze at cutoverRemoves the old-data migration risk
Platform Commons standardises on Flutter for its other appsShared components and staff outweigh per-app fit

Flip to KMP with shared iOS UI (Compose Multiplatform) if iOS is confirmed but the reviewers stay with Kotlin.

Target architecture: Compose

LayerChoiceNotes
UIJetpack Compose, Material 3, Navigation Compose (type-safe routes)One activity; design-system module with field-friendly components (large touch targets, offline banners)
StateViewModel + StateFlow, unidirectional data flowScreen state as immutable data classes; events as sealed types
DomainPure Kotlin module: use cases, CategoryGate, LandUsage, validators, season rulesRule parameters come from the config bundle; 100% unit-tested
DataRepositories per feature; mappers between DTO and entityNo god repository
Local DBRoom 2.8 (KMP-capable) with SQLCipher (sqlcipher-android), or SQLDelightLocal UUID primary keys, unique server ids, foreign keys, indexes
SyncOutbox table + one unique CoroutineWorker (expedited for "Sync now", foreground dataSync for long runs)Exponential backoff; delta pull by cursor
NetworkKtor client (OkHttp engine) + kotlinx.serialization; Auth plugin for bearer refreshOne client, one JSON library
AuthAppAuth-Android or App Service login endpoint; tokens in DataStore encrypted with Tink + Android KeystoreNo password stored
DIHilt (Android-only) or Koin (if KMP soon)
MapsMapLibre Native Android with offline tile packs (fallback: osmdroid in AndroidView)Polygon area, accuracy filter, self-intersection check in the domain module
LocationFused Location Provider; foreground service type location during walking captureReject fixes worse than the configured accuracy
CameraCameraX or ActivityResultContracts.TakePicture; one compression pipelineEXIF stripped; signature pad as a Compose canvas
FilesUpload queue in the outbox; resumable uploads to the App Service
i18nstrings.xml en/te/ta; AppCompatDelegate.setApplicationLocalesMissing-string check in CI
ObservabilityCrashlytics; redacted telemetry to the App ServiceTimber or Kermit with a redacting tree
TestingJUnit 5 or kotest, Turbine, in-memory Room, Ktor MockEngine, Compose UI tests, Roborazzi screenshots, Macrobenchmark + Baseline ProfilesMigration and outbox tests from day one
BuildVersion catalog, flavors dev/stage/prod, R8 on, CI

Target architecture: Flutter

LayerChoiceNotes
UIFlutter Material 3, go_routerDesign-system package; test on the slowest field phone
StateRiverpod (or Bloc)Immutable state with freezed
DomainPure Dart package: use cases, category gate, land usage, validatorsRule parameters from the config bundle
DataRepositories per feature
Local DBdrift with sqlcipher_flutter_libsSame schema rules as the Compose option
Syncdrift outbox table + workmanager plugin; flutter_foreground_task for long runsConsider a thin native WorkManager shim if the isolate proves flaky
Networkdio + retrofit.dart, freezed / json_serializable; QueuedInterceptor for token refresh
Authflutter_appauth or App Service login; flutter_secure_storage
DIRiverpod providers
Mapsflutter_map + flutter_map_tile_cachingPolygon logic in the domain package
Locationgeolocator with a foreground serviceAccuracy filter as above
Cameracamera or image_picker, flutter_image_compress, signature
Formsreactive_forms or flutter_form_builderValidators from the domain package
i18nflutter_localizations + ARB (intl)Convert strings.xml once with a script
Observabilityfirebase_crashlytics; redacted telemetry to the App Service
Testingflutter_test, mocktail, integration_test, golden tests, drift in-memory tests
Legacy readersqflite read of agri_db pending rows at first launchCannot read SYNC_QUEUE_DB params; relies on the drain release