You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Offline-first mileage, travel and expense tracking, built in Kotlin and Compose Multiplatform.
A standalone, fully offline app. It puts the location-engineering, offline-first and
multi-module architecture I care about into one place you can actually run.
Every screen draws from deterministic mock data, so there are zero backend calls.
Mileway is a self-contained, offline-first mileage tracker. The whole thing runs in airplane mode:
you track trips, log expenses, route approvals, and the data is still there after a restart. No
tracked code reaches for the network.
I also use it as a reference for how I build Android and KMP apps. That means Compose Multiplatform,
a multi-module clean architecture across 23 Gradle modules, MVI-style unidirectional state, Koin for
DI, Room (KMP) with DataStore, and a gms/noGms flavor split so the same code ships to both the
Play Store and F-Droid.
Highlights
🛰️ Real location engineering. The tracking pipeline fights GPS jitter and recovers from spikes,
with spike detection, four-bucket distance accounting and IMU fusion.
📴 Genuinely offline. No backend URLs, no API keys, no network calls in tracked code. It runs in
airplane mode and keeps its state in Room and DataStore.
🧩 23-module clean architecture. Feature modules never touch each other. They meet only at the
:app composition root, wired through Koin.
🌍 Kotlin Multiplatform — iOS live (V19). All feature screens run on Android and iOS from
commonMain. Background scheduling uses kmpworkmanager
(BGTask dispatcher + AppDelegate); platform services sit behind expect/actual.
🔀 One codebase, two distributions. A gms Play build and a FOSS noGms / F-Droid build, with
a dependency-prefix guard that fails the build the moment a proprietary library leaks into FOSS.
🧪 Quality gates in CI. 96 Roborazzi screenshot tests on the JVM (no emulator, no network),
Napier structured logging, detekt, ktlint and Kover, plus reproducible F-Droid release workflows.
Screenshots
All screens render from deterministic mock data. Images are recorded with
Roborazzi on the JVM, so no emulator is required
(./gradlew recordRoborazziNoGmsDebug).
Track Miles
Journey Detail
Trip Success
Full screen gallery: every screen across the feature modules, grouped by area (96 images)
Tracking
Logging & Expenses
Travel
Approvals & Payables
Payments, Events & Cards
Profile & Account
Search
Media & Assistant
App shell & Security
Plus component matrices (status cards, booking cards, PO cards, success-state variants) in
docs/screenshots/, rendered from @Preview composables by
ScreenshotCatalogTest. Every full screen above is recorded by ScreenshotGalleryTest.
Features
Every feature is fully interactive on mocked, offline data.
Area
What's inside
Tracking
Live GPS trip tracking on a foreground service (jitter suppression, spike detection, four-bucket accounting); geofenced check-in with manual fallback; saved tracks (journey/submission tabs); trip insights; hardware-events log; GPX / CSV / KML / GeoJSON export.
Travel hub, active-trip card (flight / train), upcoming bookings, plus trip & booking history surfaces.
Approvals & Payables
Approval queue with policy-violation badges and seek-clarification sheet; payables hub, multi-step create-PR / invoice flows and history surfaces.
Payments, Events & Cards
QR pay / request + history; event creation + history; card home / detail / request (KYC-lite).
Profile & Account
Account hub, advance requests, Canvas-rendered analytics dashboards, an AI assistant sheet, notification centre, permission-health screen, and a MaterialKolor theme engine.
A registry-based search that fans a query across every feature module.
Architecture
Multi-module clean architecture. Feature modules never depend on one another; they meet only at the
:app composition root. State is unidirectional. Each screen exposes a single immutable state as a
StateFlow, collected with collectAsStateWithLifecycle, and a shared ScreenState wrapper models
the loading, empty, error and content cases.
graph TD
APP[":app · composition root · navigation · Koin graph"]
subgraph Features
direction LR
FT["tracking"]; FL["logging"]; FM["media"]; FP["profile"]
FA["approvals"]; FPA["payables"]; FTR["travel"]; FAG["agent"]
FC["cards"]; FPM["payments"]; FE["events"]
end
subgraph Core
direction LR
UI["core:ui · design system + theme engine"]
DATA["core:data · Room (KMP) · DataStore"]
NET["core:network · API contracts"]
PLAT["core:platform · expect/actual services"]
SEC["core:security · root detection"]
MAPS["core:maps (+ krossmap / maplibre)"]
end
STUB[":stub · deterministic mock data"]
WEAR[":wear · Wear OS tile"]
APP --> Features
APP --> STUB
APP --> WEAR
Features --> Core
STUB --> DATA
STUB --> NET
Loading
Key patterns
commonMain-first KMP. Core modules compile for Android and iOS (iosArm64,
iosSimulatorArm64). Platform-bound tech (FusedLocation, CameraX, ML Kit, WorkManager,
BiometricPrompt, the foreground service) sits behind expect/actual interfaces in :core:platform.
Koin DI. One module per feature, and the InitKoin() bootstrap is re-entrancy-safe for both the
Android Application and the iOS entry point.
SearchProvider registry. Each feature binds a SearchProvider into Koin. The master-search
aggregator resolves getAll<SearchProvider>() and fans out, so search and the features stay decoupled.
Shared scaffolds.FormSubmissionScaffold and HistoryListScaffold standardise the create and
history flows that travel, payables, payments and events all reuse.
Navigation. Type-safe JetBrains Compose Navigation, with per-feature graphs assembled at :app.
A maps flavor dimension splits the app into a proprietary and a FOSS build:
Flavor
Maps
Google / Play / Firebase
Use case
gms
KrossMap (Google Maps / MapKit)
Firebase + Play services
Play Store build
noGms
MapLibre + offline MBTiles (no API key)
none (FOSS-clean)
F-Droid / fully offline
A dependency-prefix guard fails the build if proprietary libraries leak into the noGms classpath.
Testing and quality
JVM unit tests. 88 test classes covering ViewModels, repositories and feature logic with MockK
and Turbine, run on the noGms flavor with no emulator.
Screenshot tests. Roborazzi renders every screen across all feature modules plus the
component-preview matrices on the JVM (ScreenshotGalleryTest and ScreenshotCatalogTest, 90+ PNGs
in docs/screenshots/). They're deterministic and diff cleanly in PRs.
Static analysis. detekt and ktlint across every module, with Kover for coverage.
CI..github/workflows/ci.yml runs assembleGmsDebug and testNoGmsDebugUnitTest on every push
and PR. Separate quality, release and publish-fdroid workflows handle the gates and distribution.
Roadmap
A snapshot of where Mileway is and where it's heading. This is a portfolio/demo project, so the
roadmap reflects direction rather than commitments.
Shipped
Offline-first app on deterministic mock data (zero backend calls)
23-module clean architecture with Koin DI
Compose Multiplatform UI; commonMain core compiles for Android + iOS
Location engine (jitter / spike / four-bucket / IMU fusion) with a simulated drive source
Master search: a registry across feature modules with an aggregator, results screen and navigation
Roborazzi screenshot suite (96 screens, JVM-only), detekt / ktlint / Kover, CI + release workflows
Wear OS companion tile
iOS UI parity (V19). All feature screens in commonMain; background scheduling via
kmpworkmanager; AppDelegate + BGTask dispatcher; iOS builds and passes all CI gates.
Napier structured logging across all modules
Exploring
Exploring
Baseline Profiles for startup/scroll performance
watchOS companion & iOS WidgetKit surfaces
Instrumented (on-device) UI test tier alongside the JVM suite
Expand Roborazzi catalog to remaining edge-case states
iOS and Wear OS
iOS. Every :core:* module compiles to an iOS framework, with expect/actual services
backed by CoreLocation, Vision (OCR), UserNotifications, LocalAuthentication and BackgroundTasks.
A few proprietary integrations (in-app update, install-referrer) are stubbed with TODO(ios)
markers, and the shared Compose UI renders through a minimal SwiftUI host.
Wear OS.:wear ships a MileageTileService tile that surfaces today's distance.
The location engine
The tracking pipeline is built to suppress jitter and recover from GPS spikes:
Jitter suppression. Stationary drift gets filtered out while the anchor point is preserved.
Spike detection. An implied-speed check flags teleporting fixes instead of silently dropping them.
Four-bucket accounting.original, cleaned, abnormal and mock are each persisted per track.
Mock-location flagging. Spoofing is detectable, not just blocked.
IMU fusion. Accelerometer and gyroscope snapshots feed the post-hoc insight analyzers.
Set SIMULATE_LOCATION = true and a simulated drive source feeds believable fixes through the exact
same pipeline, so the whole tracking flow works on an emulator with no GPS hardware.
Mileway is a portfolio / demo project. All companies, bookings, cards and amounts are fictional mock data.