TransitOS — Open-Source Public Transport for Valencia
An open-source Android client for public transport, starting with Metrovalencia (FGV). Live arrivals, a trip planner, and an interactive network map — no ads, no tracking, and a provider architecture so any operator can be added without touching the app.
TransitOS is an open-source Android client for public transport. It starts with Metrovalencia (FGV) and is built to grow into a multi-operator platform — EMT buses, Renfe Cercanías, and beyond. It is free as in freedom, funded by nobody, and respects your privacy: no ads, no tracking, no analytics, ever.
Why this exists
The official transit apps for Valencia are ad-supported, account-gated, and loaded with analytics. You want to know when the next train leaves, and you get a banner ad, a login wall, and a dozen trackers. That’s the norm for public-transport software, and it doesn’t have to be.
TransitOS is the version where the app just tells you the next arrival. It’s open source under AGPL-3.0, it stores nothing it doesn’t need, and the network map renders on privacy-friendly OpenStreetMap tiles. The privacy framing is structural: there is no backend that records your searches, because the app talks to the operator’s data directly.
The provider architecture
The interesting engineering decision is the provider model. Every transit operator exposes data differently — different endpoints, different schemas, different idiosyncrasies. The trap is letting one operator’s quirks leak into the rest of the app.
TransitOS solves this with strict clean layering and a one-way dependency rule:
Presentation (app, feature-*, core-ui, core-design)
| depends on
v
Domain (core) -> models, repository contracts, AppResult
| implemented by
v
Data (provider-*, core-network, core-data) -> repository impls, Ktor, DataStore
The domain layer defines a single TransitRepository contract: the operations any transit system must support (live arrivals, trip planning, station search). Each operator lives in its own module — :provider-metrovalencia implements that contract against FGV’s data. The app depends only on the contract; it never knows which operator it’s talking to.
Adding a new operator means adding one module. Want EMT buses? Create :provider-emt, implement TransitRepository, register its Koin module in :app. Nothing else in the app changes — not the UI, not the trip planner, not the map. That separation is what makes the multi-city ambition realistic instead of a rewrite every time.
How it works
Live arrivals. Each station and line queries the provider for real-time ETAs. Results come back through a typed AppResult so the UI can distinguish loading, data, and error without a pile of nullable booleans.
Trip planner. Multiple journey alternatives with departure or arrival time modes and a configurable transfer buffer. The planner works against the provider’s contract, so every operator gets planning for free once it implements arrivals.
Network map. All stations, official line colors, and polylines rendered on OpenStreetMap tiles via osmdroid. It works as a visual index of the whole network — tap a station, see its lines and next arrivals.
Saved routes. Custom labels (“Home”, “Work”) for one-tap access to the trips you actually take. Stored locally with DataStore.
Theming and i18n. System / Light / Dark / AMOLED pure black, plus English, Spanish, and Valencian. All user-controlled, no server round-trip.
What I like about it
The provider boundary is the whole project. Most transit apps are built as a monolith against one operator’s API. Splitting the contract from the implementation means the app is genuinely reusable — the same UI and planner serve Valencia today and any other city tomorrow, with no duplication.
Strict clean architecture pays off. The one-way dependency rule (presentation → domain → data) looks like overhead on paper. In practice it’s what makes the provider model possible: nothing in the UI knows about FGV, so swapping the data source is a configuration change, not a refactor.
Privacy as architecture, not a setting. There’s no analytics SDK to disable, because it was never added. The app talks to the operator; the map talks to OSM; nothing talks to an ad company.
Stack
- Kotlin 2.0.21 with Coroutines and Flow
- Jetpack Compose (BOM-managed), Material 3, dynamic color
- Ktor 3 for networking · Koin 4 for dependency injection
- DataStore for preferences · osmdroid for the network map
- Gradle Version Catalog (
libs.versions.toml) as a single source of truth - minSdk 26 · target / compile SDK 35
Status
Active and usable. Metrovalencia is fully covered — live arrivals, trip planning, the network map, saved routes, search. Pre-built APKs are on the releases page. Not on Google Play yet; sideload the APK or build it yourself.
Source: github.com/IgnacioLD/TransitOS.