Platform | Before | After | What it really meant |
Android | XML Views + MVP | Jetpack Compose + MVI | The UI and presentation were rebuilt. Business/data stayed mostly stable. Tests got a major rewrite. |
iOS | UIKit + legacy SwiftUI + current SwiftUI | Unified modern SwiftUI direction (with UIKit encapsulated where needed) | Less glue code, fewer parallel UI patterns, easier long-term maintenance. The navigation refactor is still a big chunk. |
iOS (parallel track) | Swift < 6 assumptions | Swift 6 strict concurrency | A lot of compiler warning cleanup, large PRs, real tech debt surfaced. |
All articles
Behind The Release Notes: Migrating Android and iOS to Modern UI Architectures
6 min. read

At some point, every mobile team hits the same wall: the UI works, but the architecture underneath slows you down because of the state, navigation, tests, and the months where old and new systems have to coexist.
We hit that wall on both platforms at once: Android moved from XML + MVP to Compose + MVI, iOS consolidated three UI layers into a single modern SwiftUI architecture, and Swift 6 strict concurrency forced us to finally pay down the technical debt we’d been carrying.
This article breaks down the real decisions behind those migrations: what we rewired, what slowed us down (spoiler: navigation and tests), and what got noticeably better once the dust settled.
If you’re planning a migration and want fewer surprises, this is the version of the story that release notes don’t tell!
Migration at a glance
The common theme between both platforms is the architectural realignment from an imperative to a declarative UI, changing state flow, navigation modeling, testing strategies and presentation boundaries.
Why “Just Rewriting the UI” Wasn’t an Option
The real constraints were architectural. Both apps had UI layers that worked, but the systems underneath made change slower, riskier, and harder to test.
Android: XML + MVP
Android was built on XML Views and MVP’s bidirectional flow. Compose expects unidirectional state flow. That mismatch meant we couldn’t adopt Compose without changing the architecture around it.

iOS: three UI worlds in one app
iOS was blocked by fragmentation: UIKit, legacy SwiftUI patterns, and modern SwiftUI coexisted in the same app, creating glue code, duplicate patterns, and navigation that behaved differently depending on the screen. So the real goal became unifying the architecture, not just rewriting screens.
Android migration: Jetpack compose
New architecture (MVI)

What actually changed
What stayed stable: the business layer (DTO/DAO/mappers/use cases/storage) didn’t need structural changes. What was rebuilt: the UI layer, the presentation layer, and the tests tied to them.
Migration flow
The migration was staged deliberately :
- Build a PoC on a small module (to set conventions and validate the end-to-end approach)
- Ship new modules in MVI by default (so we don’t create new legacy during migration)
- Incrementally rewrite older MVP modules
For a long stretch, MVP and MVI coexisted in the same app. Modules could remain isolated, but navigation (and later auth) became the main “bridge” surfaces.
iOS migration: SwiftUI
What actually changed
Benefits and drawbacks from switching to SwiftUI
SwiftUI is currently the latest UI framework available to all Apple platforms. It allows easy sharing of code between all of Apple's platforms as a platform native since it is common to all of them. It shifts the programming paradigm to declarative view definitions which allows writing UI code faster than before.
That said, SwiftUI is still evolving. It can lack some advanced features, or be occasionally buggy or OS-version dependent- meaning certain issues can’t be fully fixed in app code and may only resolve with iOS updates.
In practice, teams often need to bridge gaps with UIKit/AppKit interoperability, either to cover missing capabilities or to implement workarounds for higher-impact problems. Another challenge is maturity: best practices and community patterns for building large, consistent SwiftUI codebases are still developing.
Swift 6 migration (iOS parallel effort)
Alongside the SwiftUI work, we’ve been migrating the codebase to Swift 6 strict concurrency. It’s roughly 60–70% complete, with the remaining work concentrated in harder modules like dependency injection.
This effort is more than a cleanup; it materially improves thread-safety and reduces race-condition risk. One concrete outcome: compiler warnings dropped from ~1500 to ~8.
What got better
Faster UI delivery and cleaner state for Android: Compose + MVI
A 2025 IJARSCT paper reports 30–40% less UI code and up to 50% faster implementation for complex UI components with Jetpack Compose compared to View-based UI. (IJARSCT)
Compose + MVI also shifted the UI to a unidirectional state flow. Instead of presenters pushing changes through view interfaces, the UI subscribes to a state stream and recomposes when that state changes. Compose also reduced XML parsing, removed hierarchy lookups, and made UI components smaller and more reusable .
- Faster UI implementation: Writing UI in Kotlin composables replaced XML boilerplate. The team’s experience was that many screens were 2–3× faster to build.
- More predictable state: State is modeled explicitly and the UI reflects it directly, which makes behavior easier to reason about.
- Less lifecycle friction: ViewModels retain state across configuration changes without extra wiring.
- Cleaner presentation layer: No view references, fewer interfaces, and intents make user actions explicit.
- Testing improved after the rewrite: Tests felt more stable, with a perceived ~10% speedup.
- Smaller, reusable UI building blocks: No XML parsing at runtime, fewer hierarchy lookups, and composables that can be reused across screens.
Unified SwiftUI and stronger concurrency for iOS: SwiftUI + Swift 6
The biggest improvement on iOS was consolidation. We went from three UI creation patterns coexisting: UIKit, legacy SwiftUI patterns, and modern SwiftUI, to a single modern SwiftUI direction. That removed a lot of the bridging work between “generations” of screens and made the UI stack easier to evolve as one system. What improved in practice:
- Less glue code. Fewer adapters and controller-based workarounds just to make screens talk to each other.
- More consistent architecture. Shared patterns for how screens are built, how state is handled, and how navigation is approached.
- Cleaner testing story. One primary approach for UI unit tests instead of different strategies depending on the screen’s UI framework.
- Swift 6 strict concurrency: Better thread safety and less race-condition risk; the compiler now catches issues that used to rely on manual discipline.
Lessons learned
1. The first module defines the migration
The first two Android modules took the longest. They forced the big calls: architecture shape, navigation approach, state conventions, testing patterns. After that, each new module got easier because the playbook already existed.
2. Navigation becomes the pressure point
MVP and MVI could run side by side, but the real tricky parts were navigation and authentication. Auth had to support two mechanisms during the transition, which added temporary complexity. For navigation, we used Nav3, an alpha Compose navigation library that gave us Compose-native patterns, but its evolving APIs during the migration also created churn. Navigation is where architectures meet: When it changes, the whole app feels it.
3. Test rewrites are not optional
Presenter tests became ViewModel tests. Espresso tests built around findViewById had to move to Compose semantics and test tags. The logic stayed similar. The selectors changed everywhere. Manual regression time also went up. This needs real time in the plan.
4. Parallel architectures can work
We ran MVP and MVI in production together for a long stretch. Modular boundaries and clear entry points made it manageable. The cost showed up as extra dependencies and higher mental load until the last legacy modules were gone.
5. Modern frameworks are powerful, but incomplete
Compose missed a small UI feature, so one part stayed in XML temporarily. SwiftUI has its own limits and some fixes depend on OS releases. Treat these as contained islands and keep moving.
6. Technical debt multiplies migration effort
Strict concurrency surfaced issues that had been sitting quietly in the codebase. Some PRs touched around 200 files. Compiler warnings dropped from roughly 1500 to about 8. The takeaway is simple: the longer debt sits, the more expensive migrations become.
What’s next
The migrations have passed the hardest conceptual phase. Now the focus is structural completion.
Android: finish the MVP exit
Compose + MVI is the default for new modules .
Next steps:
- Continue incremental conversion of remaining MVP modules
- Remove dual authentication mechanisms
- Remove MVP-only dependencies as modules move over
- Stabilize navigation under the Compose setup.
iOS: finish UI and navigation unification
Most SwiftUI work is already in the modern direction. Navigation is the big remaining piece, and it’s still being tested and won’t ship immediately.
Next steps:
- Keep moving UIKit screens into SwiftUI
- Complete navigation migration
- Remove remaining legacy SwiftUI patterns
Swift 6: push the last hard modules over the line
Swift 6 strict concurrency is mostly in, around 60–70% complete. The next steps include complete migration of tricky modules (DI included), prevention of warning debt from reappearing, and keeping strict checks enabled permanently.
The real end state
Across both platforms, the real end state we are aiming for is:
- One unified architecture per platform
- No transition glues left behind
- No hidden legacy layers
- No accumulated warning debt
Continue reading

7 min. read
Credential Governance in Hospitals: When Security Can’t Wait
Manage healthcare credentials safely at scale. Explore how governance tools support secure hospital operations across teams and vendors without disrupting critical patient care.
5 min. read
2025: A Year in Review
Passbolt focused on scaling infrastructure and refining the user experience throughout 2025.