Skip to content

darkpandawarrior/Mileway

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

281 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mileway app icon

Mileway

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.

CI Quality Kotlin Compose Multiplatform Platforms Offline

Highlights · Screenshots · Features · Architecture · Getting started · Roadmap


Table of contents

Why Mileway

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
Track Miles ready-to-start screen with vehicle selector and distance card Track detail with route stats, journey overview and GPS-point breakdown Tracking success summary with distance, reimbursement amount and voucher
Full screen gallery: every screen across the feature modules, grouped by area (96 images)

Tracking

     
Track Miles ready-to-start screen with vehicle selector Tracking success summary with distance, reimbursement and voucher Saved tracks journeys tab with date-grouped trip cards
     
Track detail with route stats, speed and GPS-point breakdown Track insights with quality-score ring and activity breakdown Geo check-in with map overlay and radius indicator
     
Location map surface with current-position marker Manual check-in with location, notes, type and time Check-in history list with geo and manual entries
     
Hardware-events log with tracking lifecycle entries and filter chips Track data preview overview tab with completeness metrics Tracking settings with accuracy, interval and battery options
     
Track customization with map style and overlay toggles Tracking setup guide walking through permissions Tracking loading screen with progress sub-statuses
   
Create voucher screen selecting reimbursable expenses Developer debug menu for tracking internals

Logging & Expenses

     
Spends home with totals and recent activity Manual log-miles step 1 with location search and route preview Manual log-miles step 2 with travelled locations and amount
     
Log-miles history with date-grouped mileage entries Expense entry with category, amount and attachment Expense details input with policy fields
     
Expense detail view with status and receipt Expense history with status filter chips and itemised cards Voucher history with settlement status

Travel

     
Travel hub with active trip and upcoming bookings Create trip request form with purpose and itinerary Create multi-journey plan with route legs
   
Trip-request history with status tabs and route cards Booking history with type tabs, status filter chips and fare cards

Approvals & Payables

     
Approvals pending tab with policy-violation badges Approval detail with a flagged policy violation Payables hub with invoices, PRs and GINs
     
Create purchase-request form with line items Purchase-request detail with approval trail Create invoice form with vendor and amount
 
Unified payables history across document types

Payments, Events & Cards

     
Pay or Request form with UPI payee, amount and mode toggle Payments history with status tabs and UPI pay/request cards Create event form with title, venue, category and capacity
     
Events history with status tabs and venue/attendee cards Cards home with virtual card faces and balances Card detail with transactions and controls
 
Card request form with KYC-lite fields

Profile & Account

     
Account hub with persona switcher, deep-link demo and referral card Profile details with employee information Settings with sections for app, privacy and account
     
Preferences with units, theme and notification toggles Demo settings to seed and reset mock data Analytics dashboard with Canvas-rendered charts
     
Mileage analytics detail with trend chart Advance-request history with status Ask-advance form step 1 with amount and reason
     
Delegation screen assigning approver authority Notification centre with grouped alerts QR home for scan-to-pay and identity
 
Help and support with FAQs and contact

Search

   
Master search with query, category tabs and grouped results across trips, payments and events Master search empty state prompting across all record types

Media & Assistant

     
Attachment selection with camera and gallery sources Attachment preview before attaching Media library grid of saved attachments
     
Camera capture screen with permission prompt AI assistant chat answering an expense query Assistant conversation history
 
Assistant home bottom sheet with suggested actions

App shell & Security

     
Home dashboard with greeting and quick actions Login screen with demo credentials Branded splash screen
     
Shell placeholder for an in-progress destination Root guard showing detected root signals in red Root guard confirming a clean, secure device

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.
Logging & Expenses Step-by-step manual trip logging; expense entry → detail → success chain.
Travel 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.
Media CameraX capture (flash, pinch-zoom, tap-focus), on-device odometer OCR, attachment grid.
Master search 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.

Module map

Module Responsibility
:app Composition root, navigation host, Koin graph assembly, build flavors
:core:ui Compose design system, theme engine (MaterialKolor), Canvas charts, shared scaffolds
:core:data Room (KMP) database, DAOs, entities, DataStore repositories
:core:network API contract & policy models (mocked)
:core:platform expect/actual platform-service interfaces + Android/iOS impls
:core:security Device-integrity (root) detection, encryption-ready storage
:core:maps · -krossmap · -maplibre Map-surface contract + flavor-specific implementations
:core:common Shared utilities / primitives
:feature:* tracking · logging · media · profile · approvals · payables · travel · agent · cards · payments · events
:stub Deterministic mock data for every repository (no backend)
:wear Wear OS companion tile
build-logic Gradle convention plugins (centralised AGP / Kotlin / Compose config)

Project structure

Mileway/
├── app/                      # Android application: composition root, navigation, DI, flavors
├── core/
│   ├── ui/                   # Compose design system, theme engine, shared scaffolds
│   ├── data/                 # Room (KMP) + DataStore
│   ├── network/              # API contracts (mocked)
│   ├── platform/             # expect/actual platform services
│   ├── security/             # root detection, encryption-ready storage
│   ├── maps/ maps-krossmap/ maps-maplibre/   # map-surface contract + impls
│   └── common/               # shared utilities
├── feature/                  # tracking · logging · media · profile · approvals
│                             # payables · travel · agent · cards · payments · events
├── stub/                     # deterministic mock data for every repository
├── wear/                     # Wear OS companion tile
├── build-logic/              # Gradle convention plugins
├── docs/                     # README assets, screenshots, release & brand docs
└── fastlane/                 # store metadata + screenshots

Tech stack

Layer Technology
Language Kotlin 2.4.0
UI Compose Multiplatform 1.11.1, Material 3
Build AGP 9.2.1, Gradle Kotlin DSL, convention plugins, version catalog
DI Koin 4.2.2 (multiplatform)
Database Room 2.8.4 (KMP, bundled SQLite)
Settings / session AndroidX DataStore
Networking Ktor 3.5.0 (OkHttp + Darwin engines), mocked with no live backend
Concurrency Coroutines + Flow (no LiveData); kotlinx-datetime 0.8.0 in commonMain
Maps osmdroid / MapLibre (noGms, offline MBTiles) · KrossMap (gms)
Charts Canvas-only (no MPAndroidChart / Vico)
Testing JUnit, MockK, Turbine, Robolectric, Koin-Test, Roborazzi 1.64.0 screenshots
Quality detekt 1.23.8, ktlint, Kover, dependency-guard
SDK compileSdk 37, minSdk 30, JDK 21

Getting started

git clone https://github.com/darkpandawarrior/Mileway.git
cd Mileway

# Assemble the offline-safe default build
./gradlew assembleNoGmsDebug

# Install on a device/emulator (API 30+)
adb install app/build/outputs/apk/noGms/debug/app-noGms-debug.apk

No network connection is required. The data is all mock and persists locally through Room and DataStore.

Offline check: enable airplane mode, track a trip, kill and relaunch the app, and confirm the record persisted.

All build & tooling commands
# Build variants
./gradlew assembleNoGmsDebug          # FOSS / offline build (default)
./gradlew assembleGmsDebug            # Google-services build
./gradlew assembleNoGmsRelease        # reproducible FOSS release (F-Droid)

# Tests & screenshots (noGms only; gms crashes Robolectric)
./gradlew testNoGmsDebugUnitTest      # JVM unit tests (88 test classes, no emulator)
./gradlew recordRoborazziNoGmsDebug   # (re)record screenshot baselines → docs/screenshots/

# Quality
./gradlew ktlintCheck detekt          # style + static analysis
./gradlew :app:koverXmlReport         # coverage report

Build flavors

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
  • gms / noGms flavor split + FOSS dependency-prefix guard
  • Room (KMP) + DataStore persistence
  • 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
  • Accessibility pass (TalkBack, semantics, large-font / RTL)
  • Larger bundled offline map packs
  • 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.

About

Mileage & trip tracking app — Compose Multiplatform, sensor-fusion location engine, ML Kit OCR, offline-first

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages