Red Regional
Projects | | Links:

Red Regional brings trip planning and transport information to regional public transport systems across Chile. It’s provided by the División de Transporte Público Regional (DTPR).
Red Regional is a public transport app for towns and provinces across Chile outside Santiago, provided by the División de Transporte Público Regional (DTPR). It offers trip planning, service information, and payment flows across regional networks — and it started its life as two separate native apps in Kotlin and Swift.
I led this project as mobile tech lead, heading a team of three engineers through a full migration from the native codebases into a single Flutter app. Beyond coordinating the team and reviewing work, I took ownership of the most critical pieces: defining the technical rules for the migration and solving the hardest problem we had — preserving all existing user data across the transition.

Context and problem
Before the migration, Red Regional existed as two independent native apps — one in Kotlin for Android, one in Swift for iOS. Features diverged over time, bugs were fixed twice, and any new capability had to be built and maintained in parallel. The gap between the two versions was a constant friction point for both the team and the product.
The business case for migrating to Flutter was straightforward: one codebase, consistent behavior across platforms, and faster delivery. But migrations in production apps carry real risk. The main constraint was that existing users couldn’t lose their data — preferences, saved stops, and account state all had to survive the transition cleanly.
How it works
The Flutter app replaced both native apps with a single codebase that targets Android and iOS from shared logic. Region-specific behavior is handled through a configuration layer, which controls feature availability and local integration points without branching the core product.
The data migration layer was designed to read each platform’s native storage format on first launch after the update, transform it into the new schema, and write it to Flutter’s persistence layer — transparently, before the user ever sees the main screen.
Challenges
Migrating a live app with real users is a different problem than starting from scratch.
- The Android and iOS versions had diverged in subtle ways — data schemas, stored formats, and edge cases that weren’t documented anywhere. Mapping both into a single unified model required careful auditing of both codebases before a line of Flutter was written.
- Data preservation had to be bulletproof. A failed or partial migration would corrupt user state with no easy recovery path, so the process needed to be atomic, well-tested, and fail-safe.
- Coordinating three engineers on a migration means managing dependencies carefully. Some work could be parallelized; other pieces — especially anything touching the shared data layer — had to be sequenced and reviewed tightly.
- The Flutter app needed to reach feature parity with both native versions before launch, which required a clear inventory of what each platform had built independently over time.
Key decisions and trade-offs
The first decision I made was to define the migration rules before writing any product code. That meant agreeing on the target architecture, the data model, and the boundaries between shared and platform-specific code as a team, so no one had to make those calls in isolation mid-sprint.
For the data migration, I chose an atomic, one-shot approach executed on first launch: read native storage, transform, write to Flutter persistence, then delete the old data. The alternative — a gradual sync — would have been harder to test and more likely to leave the app in a partially migrated state.
| Decision | Trade-off |
|---|---|
| Single Flutter codebase replacing both native apps | Unified delivery and consistent UX, at the cost of an upfront migration investment |
| Atomic first-launch data migration | Reliable and testable, but no easy rollback if something goes wrong post-release |
| Architecture rules defined before implementation | Faster team alignment and fewer integration surprises, but required upfront investment from everyone |
| Feature inventory before coding | Prevented parity gaps at launch, but added time to the planning phase |
What I learned
Leading a migration is different from leading a greenfield project. The constraints are tighter because you’re accountable to existing users, and the decisions you make early — about data, architecture, and team coordination — have outsized consequences later.
The most valuable thing I took from this project was the discipline of defining boundaries and rules before distributing work. On a three-engineer team, ambiguity in the shared layer is expensive. Clear ownership and explicit contracts between modules made the migration predictable enough to actually ship.
I also learned that native-to-Flutter migrations surface assumptions that were never written down. Auditing both codebases thoroughly at the start felt slow, but it prevented the kind of late-stage surprises that derail a release.
Tech stack
- Flutter for the cross-platform mobile client (migrated from Kotlin and Swift).
- Platform channels for reading legacy native storage during the migration.
- Mapbox for maps and rendering.
- Firebase for analytics and remote configuration.
Closing reflection
Red Regional was the first full migration I led from native to Flutter, and it sharpened my understanding of what that kind of work actually involves — technically and organizationally. Getting three engineers to a clean, consistent codebase without dropping anything users depended on was the real challenge. The app being faster to develop and more consistent across platforms afterward was the payoff.