- Kotlin
- Jetpack Compose
- Material 3
- MVVM + UDF
- Room
- Retrofit
- Coroutines
- StateFlow
- Navigation Compose
The shape of it
- 8 Steps in the fuelling wizard, each with its own validation gate
- 2 Roles, separated at the database query, not just in the interface
- ±200 Pounds of tolerance on per-tank final readings
The board shows active orders sorted by departure, with three independent controls that combine freely: status filter, concourse filter, and sort by departure time, gate, or fueller. A team leader sees every order and can assign, reassign, or unassign fuellers. A fueller sees only their own.
What it does
-
Eight-step fuelling wizard
Modelled on the real ramp process: tail verification, equipment entry, per-tank arrival readings, pumping, per-tank final readings, fuel cap safety check, gallon-based totaliser, and close-out by employee ID.
-
Per-aircraft tank layouts
Tank configuration comes from the order data. Boeing narrowbodies show three tanks, left, centre, and right. Airbus adds ACT 1 and ACT 2 for five. The interface adapts on its own rather than asking the fueller to pick.
-
Role-based access
Team leaders get assignment controls and station-wide statistics. Fuellers get their own queue and the fuelling workflow, and nothing else.
-
Status history
Every status transition is timestamped and logged against the order, so the sequence can be read back after the fact.
-
Statistics
Completed order counts, total fuel pumped, and per-fueller workload, aggregated with SQL rather than by pulling rows into memory and looping.
Offline-first architecture
The interface never reads from the network. It only ever observes the local Room database. A remote JSON source feeds Room, and the interface reacts to Room.
Retrofit → Repository → Room (source of truth) → StateFlow → Compose UI
Lose signal on the ramp and the board still works from cache. Changes also propagate on their own: complete an order and it leaves the board without any code that refreshes a list.
Layers are kept honest by dependency direction. Domain models are plain Kotlin with no Android, Room, or Retrofit imports. The data layer holds the remote API, the Room entities, the repository that maps between them, and in-memory session state. Each screen has one ViewModel exposing a single immutable state object.
Decisions worth defending
-
Derived state, not stored state
Whether an order is overdue is never written down. It is computed from departure time against now. Fuelling totals work the same way. Store the facts, derive the conclusions.
-
A refresh never overwrites work
Pulling new remote data uses
OnConflictStrategy.IGNORE, so existing rows are left alone and local status changes survive. Local is the source of truth; a sync that clobbers what the user just did is a bug, not a feature. -
Authorisation lives in the query
Fuellers cannot see assignment controls, and they cannot reach another fueller's orders because the query filters by fueller ID. Hiding a control is user experience. The real boundary is in the data layer.
-
Nothing persists until close-out
The wizard holds its data in memory and writes to Room only at the end, so an abandoned wizard leaves no partial order behind.
Scope and limits
These are deliberate, and I would rather name them than let someone assume otherwise.
-
Login is a demonstration
Selecting a role from mock data demonstrates authorisation — who can see and do what. It is not authentication. Password hashing, server-side verification, and tokens all need a backend, which this project does not have.
-
Destructive migrations
fallbackToDestructiveMigration()is fine here because the cache re-syncs from remote. Anything carrying real user data would need proper Room migrations first. -
Simplified tank rules
Tank layouts are modelled, but inter-tank rules such as maximum imbalance and required fill order are out of scope on purpose, to keep the demonstration on the dispatch workflow.
-
Manual dependency injection
A small application-scoped container wires everything by hand. Right size for this, and Hilt is the obvious next step if it grows.
Built with mock data only. No real company data, branding, or proprietary systems are used anywhere in this project.
What I took from it
Knowing the workflow decided the architecture. The eight steps are not a design invention — they are the order the job actually happens in, including the fuel cap check that exists because of what happens when it is skipped.












