Soil Cub shipped as a native Android app, written in Kotlin with Jetpack Compose. It works well. So the reasonable question is: why am I rebuilding it in Flutter?
The honest answer starts with a fact about the team. The team is one person. I'm the sole developer and maintainer, and the product needs to be on both Android and iOS. That single reality reframes the whole decision.
The math of a one-person mobile team
Two native codebases means writing every feature twice, fixing every bug twice, and keeping two languages, two toolchains, and two release processes in my head. For a large team with an Android squad and an iOS squad, that cost is manageable. For one person maintaining an app long-term, it's the thing that slowly eats all your time.
Flutter's proposition is simple: one codebase, both platforms. Write the feature once, ship it to Android and iOS. For my situation, that's not a preference — it's what makes maintaining two platforms sustainable at all.
Why now, and why it isn't a rewrite
People hear "migration" and picture a risky from-scratch rewrite. It isn't, and the reason is the architecture I described in the offline-first post. Soil Cub was built with clear boundaries: a BLE layer that only gets data in, a local database as the single source of truth, and a UI that just observes that state.
Those boundaries are language-agnostic. The decisions — what the data model is, how a reading flows from sensor to storage to screen, what "offline-first" guarantees — all carry over unchanged. What changes is the implementation underneath:
- UI: Jetpack Compose → Flutter widgets.
- Storage: Room → a Dart persistence layer (e.g. Drift/sqflite), same schema, same "database is truth" rule.
- BLE: the Android BLE APIs → a Flutter BLE plugin, with platform channels where I need to reach native capabilities directly.
Because the shape of the app is preserved, the port is a re-implementation against a known design — not a blank page. That's the payoff of getting the architecture right the first time: it survives a change of language.
The honest tradeoffs
I don't think there's a free lunch here, so it's worth being clear about what I'm giving up and what I'm buying.
- BLE is the riskiest part. Bluetooth on mobile is fiddly natively; through a cross-platform plugin it's fiddlier. This is where I spend the most care, and where platform channels earn their keep.
- Native gets platform features first. A brand-new OS API lands in Kotlin/Swift before it lands in a Flutter plugin. For this product, that's rarely on the critical path.
- What I gain: iOS parity without a second codebase, faster UI work, and — most importantly — one surface for a solo maintainer to keep healthy for years.
For a solo-maintained product that has to live on both platforms, reach and maintainability beat having the newest native API a few months early.
What's next
The same Flutter foundation is what WeatherCub — the companion app for EarthScout's weather station — will be built on from the start. Migrating Soil Cub isn't just about one app; it's about setting up a single, shared way of building every app in the family, so a one-person team can credibly ship and maintain all of them.
The takeaway
Choosing a framework isn't really a technology decision; it's a decision about who has to maintain the result and for how long. Native Kotlin was the right call to prove Soil Cub. Flutter is the right call to sustain it — and the fact that the migration is tractable at all is the clearest evidence that the original architecture was sound.