Red
Projects | | Links:

Red is the official mobility app for Santiago that brings trip planning, real-time predictions, stop alerts and modern payment methods (bip! QR and automatic top-ups) into a single, user-friendly mobile experience.
Red (Red Metropolitana de Movilidad) is Santiago’s official mobility app, used by millions of commuters to plan trips, get live arrival predictions, and pay across buses, Metro, and commuter rail. Like Red Regional, it started as two separate native apps — one in Kotlin, one in Swift — and needed to be unified into a single Flutter codebase.
This was the second migration I worked on as mobile developer, and it was significantly more complex. Red carried deep integrations with native libraries that had no Flutter equivalent: a QR payment SDK for fare payment in Santiago’s transit network, and an Android-only NFC library for reading and charging Bip! cards directly from the device. Preserving those integrations while migrating the rest of the product to Flutter was the central engineering problem.

Context and problem
Santiago’s public transport system runs on Bip! — a contactless card used to pay for buses, Metro, and rail. The existing Red apps had spent years building integrations around it: QR-based fare payment, NFC card reading on Android, balance top-ups, and connections to multiple third-party recharge providers. Each of those integrations was built natively.
Migrating to Flutter without disrupting any of that was the constraint that shaped everything. The goal was the same as Red Regional — a single codebase, faster delivery, consistent behavior across platforms — but the integration surface was far more complex, and some of it was fundamentally Android-only.
There was also an operational challenge specific to Red: the GTFS feed for Santiago is updated frequently, and the app’s trip planning and prediction system depends on it staying in sync. Any degradation there is immediately visible to users.

How it works
The Flutter app handles all product UI and business logic. Native libraries are wrapped through platform channels — one channel for the QR payment SDK used across both platforms, and a separate Android-only channel for the NFC/Bip! card operations. On iOS, Bip! card functionality is either absent or handled through alternative flows, since the NFC library has no iOS counterpart.
Bip! top-ups connect to multiple recharge providers, each with its own integration protocol. The app abstracts those differences behind a unified recharge flow, routing to the appropriate provider based on the user’s selected method.
GTFS data feeds the trip planning and prediction engine. Because Santiago’s feed is updated frequently, the client is designed to handle refreshes gracefully — falling back to cached data when a new version is still being processed, without breaking active trip planning sessions.
Challenges
The QR payment and Bip! NFC libraries were the most constrained part of the migration. Neither had a Flutter plugin, so both required building platform channel wrappers from scratch — maintaining the native code alongside the Flutter layer, and ensuring the contract between them was stable and well-tested.
The Android-only nature of the NFC library created a permanent platform split that had to be managed deliberately. The feature needed to be available on Android, absent or gracefully degraded on iOS, and both states needed to be handled in the shared Flutter code without leaking platform conditionals everywhere.
Connecting to multiple Bip! recharge providers added integration complexity that compounded over time. Each provider had its own authentication flow, error model, and edge cases. Building a unified layer on top of them — one that felt consistent to the user — required careful abstraction and thorough testing across all paths.
GTFS feed volatility was a different class of problem. Frequent updates meant the local cache could become stale unexpectedly, and the trip planner had to handle mid-session feed changes without crashing or showing incorrect routes.
Performance on older Android devices remained a real constraint, as it had been with the native apps. Map rendering, route recalculation, and payment flows all had to be optimized for devices with limited memory and CPU.
Key decisions and trade-offs
The decision to wrap native libraries via platform channels rather than rewrite their functionality in Dart was the most consequential call on this project. Rewriting wasn’t realistic given the complexity and regulatory requirements around the payment SDKs, but platform channels introduced a maintenance boundary that had to be managed carefully.
For the Android-only NFC feature, we chose explicit feature flagging over runtime capability detection. This made the platform split visible and testable, at the cost of some additional branching in the codebase.
| Decision | Trade-off |
|---|---|
| Platform channels for native payment and NFC libraries | Preserved complex integrations, but added a maintenance boundary between Flutter and native layers |
| Android-only NFC via explicit feature flags | Clear and testable platform split, but permanent divergence between Android and iOS feature sets |
| Unified recharge abstraction over multiple providers | Consistent UX across providers, but more integration surface to maintain and test |
| Cached GTFS with graceful refresh handling | Resilient trip planning during feed updates, but added complexity to the data layer |
What I learned
This migration taught me that platform channels are a powerful tool, but they shift the problem rather than eliminate it. The native code still needs to be maintained, the contracts need to be tested, and any change to the underlying SDK can break the Flutter layer in ways that are hard to catch without good integration tests.
Managing an Android-only feature in a cross-platform app is also a design problem, not just an implementation one. Getting it right required being explicit about what the app could do on each platform, from the architecture level down to the UI copy.
Working with multiple recharge providers reinforced something I had already learned from Red Regional: the complexity of external integrations compounds. Each one adds failure modes, edge cases, and coordination overhead that only become visible once you’re deep in the implementation.
Tech stack
- Flutter for cross-platform mobile development.
- Platform channels for wrapping the QR payment SDK and the Android NFC/Bip! library.
- Mapbox for map rendering and route visualization.
- Firebase for analytics, crash reporting, and remote configuration.
- Multiple third-party Bip! recharge provider integrations.
- Santiago GTFS feed with client-side caching and refresh handling.
Closing reflection
Red was a harder migration than Red Regional, and the difficulty was mostly in what couldn’t change: the native payment integrations were fixed constraints that the Flutter codebase had to work around rather than replace. Getting that right — without degrading the user experience or breaking the payment flows millions of people depend on — was the real achievement of this project.