When "just build the app" turns into four apps overnight
We all know the feeling. The Android version is finally coming together, and then the inevitable request drops: “Can we get this on iPhone too?” A week later, it’s “What about Wear OS?” Then Apple Watch joins the chat. And because the universe has a sense of humor, someone asks, “Could Siri or Assistant just answer quick questions?”
Suddenly, you aren't building a single app anymore. You’re managing a whole ecosystem of apps, devices, APIs, permissions, sync rules, and UX expectations that desperately want to drift apart the second you look away.
That’s exactly why the gcaguilar/bizimobile repository is so interesting. It’s not just another simple bike-station app for Zaragoza. It reads like a practical, battle-tested reference for shipping a cohesive product across Android, Wear OS, iOS, and Apple Watch—all while keeping the messy logic exactly where it belongs: shared. (GitHub)
The Pain: The headaches this repo actually solves
The real challenge this project tackles isn't showing bike stations on a map. That’s the easy part. The hard part is everything that surrounds it:
- Duplicated logic: Writing the exact same business rules for Android and iOS.
- Different UI stacks: Managing entirely different interfaces for phones and watches.
- Location limbo: Dealing with users denying permissions or GPS taking forever to lock on.
- Flaky APIs: Relying on public civic data sources that aren't always in a great mood.
- Cross-device sync: Making sure a "favorite" on your phone actually shows up as a "favorite" on your watch.
- Voice integrations: Trying to add Siri or Assistant without accidentally building a second, parallel version of your app.
bizimobile tackles all of these head-on. By splitting responsibilities into shared:core, shared:mobile-ui, androidApp, wearApp, and apple, it sends a strong architectural message: share the logic and as much UI as possible, but keep native shells for the parts that truly need native APIs. The shared core uses Kotlin Multiplatform (KMP) with Ktor, serialization, Okio, and Metro DI. Meanwhile, the shared mobile UI is built with Compose Multiplatform and exported to iOS as static frameworks. (GitHub) The practical value here is huge: it stops the classic “same feature, four different implementations” trap before it even starts.
The Solution: Shared where it matters, native where it pays off
What makes this repo so elegant is that it doesn’t force everything into a single, massive abstraction blender. Instead, it uses a highly pragmatic split:
1. Shared core for domain and data
The shared core owns the models, repositories, configuration, platform contracts, API clients, station matching, settings, and assistant-resolution logic. In plain English: the rules of the app are written exactly once. (GitHub)
2. Shared mobile UI for Android and iOS
There’s a dedicated shared:mobile-ui module that relies on the core, uses Compose Multiplatform, and gets exported as an iOS framework. Android loads that UI directly, while the Apple side imports BiziMobileUi and wraps it with SwiftUI/App Intents glue. It’s the perfect middle ground—one product experience, without pretending iOS and Android are identical under the hood. (GitHub)
3. Native shells for platform superpowers
On Android, MainActivity creates platform bindings, feeds them into the shared UI, parses shortcut payloads, and handles location permissions. Over on Apple, Swift files wrap the shared graph for Siri, App Intents, and WatchConnectivity. For Wear OS, the watch app builds the same shared graph but renders a watch-specific Compose UI. (GitHub)
It’s the kind of architecture that feels grown-up: shared brain, native hands.
How it works under the hood
1) The data layer is built for reality, not the happy path
One of the smartest details in the repo is how it fetches station data. It tries the official Zaragoza City Council feed first. If that fails, it quietly falls back to CityBikes. It handles pagination, merges results, filters out out-of-service stations, calculates your distance, and sorts by proximity. (GitHub)
It’s an excellent example of defensive product engineering. It’s like carrying an umbrella in a city where weather apps lie to you—not glamorous, but deeply practical.
2) GPS gets a timeout (because users hate waiting)
StationsRepositoryImpl does something every app should do: it refuses to block the whole UI while waiting for a perfect GPS fix. It tries to get your location, but only gives it a short timeout. If it fails (or if the user says "nope" to permissions), it falls back to a default location in the city center. The user immediately gets useful data instead of staring at an endless loading spinner. (GitHub)
3) Favorites are actually synchronized, not just stored
Favorites are treated as shared user state. The repository saves a JSON snapshot locally, pulls the latest from the watch sync bridge, merges them, deduplicates, writes back to disk, and pushes the update back to the watch. On Apple platforms, it uses WatchConnectivity to turn the watch-and-phone relationship into a real, two-way sync channel, not just a fake "companion app." (GitHub)
4) The Dependency Injection is intentionally boring
The repo uses Metro DI for compile-time dependency injection. If you haven't used compile-time DI, think of it like wiring a house before you move the furniture in: everything is securely connected ahead of time, rather than relying on runtime magic to find the right outlet. This keeps the shared layer completely portable without blinding it to native capabilities. (GitHub)
5) Voice shortcuts aren't just bolted on
Instead of duplicating logic for voice assistants, the shared core defines the actions ("nearest station", "station status", etc.) and handles the query matching. On Apple platforms, the AppleShortcutRunner simply asks the shared KMP graph for the answers. This is exactly how you avoid the dreaded "Siri says one thing, but the app shows another" bug. (GitHub)
6) Wear OS is a first-class citizen
The Wear app isn't an afterthought. It builds the shared graph, refreshes data, and lets users browse stations and launch routes using a UI actually built for a watch screen. It proves a great point: shared logic doesn't mean shared screens; it means shared decisions. (GitHub)
Why you should study this (even if you don't care about bikes)
The bike-network use case is just the vehicle. What you’re really looking at is a highly reusable blueprint for apps that need:
- Cross-platform domain logic
- Shared mobile UI
- Native integrations at the edges
- Resilient API consumption
- Real watch/mobile state sync
- Assistant and shortcut support
Even the CI/CD pipeline is mature, building Android, iOS, and watchOS jobs in parallel and distributing them through Firebase. That’s release-minded engineering, not toy-project behavior. (GitHub)
The Takeaway
bizimobile is worth your time because it showcases a version of Kotlin Multiplatform that is practical, not ideological. It proves you can share the heavy lifting, keep the native UX where it counts, and build something that actually behaves coherently across phones, watches, and voice assistants. (GitHub)
If you get even one "ah, that's a smart way to do it" moment out of it, go drop a star on the repo. The best technical examples aren't the ones shouting the loudest—they're the ones quietly solving the messes we deal with every day.
Top comments (0)