Inspiration
Anyone who's waited for a BT bus in February knows the feeling. The official tracker is slow, schedules are buried in PDFs, and service alerts live on a separate page nobody thinks to check. Bloomington Transit publishes real-time GTFS data — it just doesn't surface in a way riders actually use. We wanted the app we wished existed every time we squinted at a paper schedule in the cold.
What it does
Easy Transit is an Android app for BT riders:
- Live vehicle positions updated every 10 seconds
- All 13 active routes with expandable stop schedules
- Service alerts auto-refreshed every 30 seconds
- Destination search across Bloomington landmarks and stops
- Four-tab navigation: Home, Map, Schedule, Alerts
It runs entirely on BT's public GTFS and GTFS-Realtime feeds. No API keys, no login.
How we built it
Kotlin with Jetpack Compose and Material 3. The app follows MVVM with a repository layer:
- Data layer —
GtfsRtClientfetches the three protobuf feeds (position_updates.pb,trip_updates.pb,alerts.pb) over OkHttp and decodes them with the official GTFS-RT schema.RealtimeRepositoryexposes these as Kotlin Flows on a 10-second tick (30 for alerts). - Domain layer — plain Kotlin models (
Vehicle,TripUpdate,ServiceAlert) with no Android dependencies. - UI layer — one ViewModel per screen, consuming repository flows as
StateFlow. Screens are stateless composables that render whatever the ViewModel emits.
State is hoisted, screens are pure, and repositories are the single source of truth for live data. The architecture is deliberately simple so we could move fast without fighting the framework at 3 AM.
Challenges we ran into
- Protobuf edge cases. GTFS-Realtime messages often arrive half-populated — vehicles with no assigned trip, trip updates missing stop sequences. Our first parser crashed on roughly a third of real traffic. We rewrote it to treat every field as optional and skip malformed entries instead of failing the batch.
- Three feeds, one UI. Merging vehicle positions with trip updates to produce accurate ETAs meant joining streams by trip ID in real time. We landed on a
combineflow that recomputes on either feed's update. - Polling without draining the battery. Three repositories on different intervals, each scoped to the screen that needs them, took more iteration than we expected. We tied collection to lifecycle so polling stops when the app is backgrounded.
- Time. With 24 hours and four people, we prioritized the data layer end-to-end over surface polish. The Map screen is a scaffolded placeholder the data pipeline behind it is complete.
Accomplishments that we're proud of
- A live data layer that holds up under real traffic: three polling feeds, merged, parsed, and rendered without blocking the UI thread.
- Architecture that compounded. By the time we built the Alerts screen — our fourth — we were moving noticeably faster than on the first, because the repository and ViewModel patterns were settled.
- Four people on one repo in 24 hours without a merge-conflict disaster, because we agreed on the package layout in the first hour.
- An app that works end-to-end on real BT data today. Open it at a stop, see the bus coming.
What we learned
GTFS has depth once you move past the basics — service IDs, calendar exceptions, and direction IDs all turned up before we expected them. Compose handles high-frequency state updates at 10-second cadence with no recomposition tuning. And treating the repository layer as the hard problem — not the UI — let everything else fall into place.
What's next for Case - 3 Bloomington Transit App (Easy Transit)
- Full interactive map with live route overlays and tap-to-track on individual vehicles
- Geofenced arrival alerts — notify when a tracked bus is within a user-set distance of a chosen stop
- UMO app handoff for fare payment, next-day schedule view, and departure reminders
- Route favorites, an arrival planner between two stops, and local trip ratings
The data pipeline is the hard part. Everything above is UI work on top of plumbing that already exists.
Built With
- android
- android-studio
- gtfs
- gtfs-realtime
- jetpack-compose
- kotlin
- kotlin-coroutines
- kotlin-flow
- material-3
- mvvm
- okhttp
- protobuf
Log in or sign up for Devpost to join the conversation.