Inspiration
Every IU student and Bloomington resident who has stood at a bus stop wondering "where is my bus?" inspired this project. The official Bloomington Transit website isn't built for real-time tracking — you're left guessing. We wanted to build the app that riders actually need: open your phone, see exactly where every bus is, and know when it's arriving at your stop.
What We Built
A fully native Android app that integrates with Bloomington Transit's public GTFS and GTFS-Realtime APIs to deliver:
- Live Map — Every active bus plotted on Google Maps, updating every 10 seconds
- Bus Tracker — Tap any bus to lock onto it and follow its live position
- Schedule — Upcoming arrivals by route with real predicted arrival times
All data is 100% real — no mocks, no placeholders.
How We Built It
We used Kotlin and Jetpack Compose for the UI, following MVVM architecture with a Repository pattern. The data layer works in two parts:
Static GTFS data — On first launch we download Bloomington Transit's GTFS
ZIP file, parse stops.txt, routes.txt, trips.txt, and shapes.txt, and
cache everything into a local Room database. Every subsequent launch is instant.
Live GTFS-Realtime data — A Retrofit + OkHttp client polls two binary protobuf feeds every 10 seconds:
position_updates.pb→ live bus coordinatestrip_updates.pb→ predicted arrival times
We parse the binary feed using the gtfs-realtime-bindings library, resolve
trip IDs to route names using our Room cache, and expose everything to the UI
via Kotlin StateFlow so screens update automatically on every poll.
The arrival time calculation works by taking the Unix timestamp from the trip updates feed and computing:
$$\text{minutes away} = \frac{t_{arrival} - t_{now}}{60}$$
where $t_{arrival}$ is the predicted arrival Unix timestamp and $t_{now}$ is the current system time in seconds.
Challenges We Faced
GTFS-Realtime feed format — The Bloomington Transit feed sends delay-based updates rather than absolute timestamps, and the trip updates don't include route IDs — only trip IDs. We had to build an in-memory cache that maps trip IDs to route IDs using the static GTFS trips table, resolved at query time.
Dependency conflicts — Our project used AGP 9.1.1 which is very new and
incompatible with kapt. We had to migrate to KSP for Room's annotation
processing and work around several plugin resolution issues before getting
a clean build.
Empty stop IDs in the feed — The trip updates feed doesn't include stop IDs in the stop time update entries, so we couldn't directly look up stop names. We pivoted to showing trip headsigns (directions) instead, which turned out to be more useful for riders anyway.
Time pressure — Building a full data pipeline from raw binary protobuf feeds to a polished UI in 24 hours required clear task separation and parallel work across the team.
What We Learned
- How GTFS and GTFS-Realtime work as the global standard for transit data
- How to architect a real-time polling app cleanly with Kotlin Coroutines and StateFlow
- That real-world public APIs are messy — the feed format didn't match the spec in several ways, and handling that gracefully is the real engineering challenge
Built With
- android
- google-maps-sdk
- gtfs-realtime
- jetpack
- kotlin
- ksp
- mvvm
- okhttp
- retrofit
- room
- stateflow
Log in or sign up for Devpost to join the conversation.