跪拜 Guibai
← All articles
Frontend

From MVP to MVI, Java to Kotlin: The Migration Pitfalls That Cost a Year

By 疯华整猫 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Teams adopting MVI and Kotlin on legacy Android code should budget for a two-week first page, a three-day learning curve on subsequent pages, and a complete rewrite of presenter tests. The biggest hidden cost is retraining developers to stop manipulating views directly.

Summary

Migrating an Android project from MVP to MVI and from Java to pure Kotlin is not a framework swap. The core change is the direction of data flow: from two-way imperative calls where a Presenter pokes at View methods, to a unidirectional loop where UI emits Intents, a ViewModel produces State, and UI renders from that State. That shift forces developers to abandon direct View manipulation, and the source reports that anyone comfortable with callbacks will struggle.

The Java-to-Kotlin conversion introduces platform types that don't enforce null safety, leading to runtime NPEs that are harder to trace. Auto-converted code compiles but isn't safe. The MVI migration also requires modeling mutually exclusive states, which data classes handle poorly, and events like toasts must be separated from StateFlow to avoid re-triggering on rotation. Coroutine misuse, such as swallowing CancellationException or ignoring race conditions, creates subtle bugs. The source recommends progressive migration, starting with a simple page, and rewriting all Presenter tests because old tests are obsolete.

Takeaways
— Platform types from Java don't enforce null safety; explicitly declare nullable or non-null types after conversion.
— Auto-converted Kotlin from Android Studio compiles but isn't safe; manually review every Java-interop return type.
— Use @JvmOverloads for Kotlin default parameters that Java callers still invoke.
— Use @JvmStatic for companion object methods that Java code calls statically.
— Migrate in order: model and utility classes first, then Repository, then Presenter/View layer.
— State in MVI should be a single data class, but sealed classes model mutually exclusive states better; they break copy() so use data class with nullable fields or separate StateFlow for add-on states.
— One-shot events (toast, navigation) should go through Channel with receiveAsFlow(), not StateFlow, to avoid replay on rotation.
— Rethrow CancellationException in catch blocks; swallowing it shows erroneous error messages when a coroutine is cancelled.
— Cancel previous jobs or use requestId to handle overlapping searches.
— Use viewModelScope for ViewModel work; Repository tasks longer than the ViewModel need their own scope with clear cancellation ownership.
— Start migration on a non-core module like Settings or About to establish the template, then replicate.
— Presenter tests with Mockito are obsolete; MVI tests send Intent and assert the StateFlow value using runTest.
Conclusions

The most time-consuming part of the migration is refactoring discrete UI commands into a single state object, suggesting the difficulty lies not in Kotlin syntax but in the discipline of modeling all UI conditions as data.

The advice to start with a simple, non-core page and accept a two-week cost for the first one indicates that migration costs are front-loaded; the second page takes three days, so the slope flattens quickly once a template exists.

Separating state from events is a general pattern for any unidirectional data flow: putting one-shot events in StateFlow creates sticky-event bugs that surface on rotation, making Channel a better fit for transient UI feedback.

The source's warning about coroutine races implies that MVI's asynchronous nature amplifies existing concurrency issues from MVP, so teams must explicitly manage cancellation and request ordering or hide subtle bugs.

Concepts & terms
Platform type
A Kotlin type that comes from Java with unknown nullability, written as String!, forcing the developer to decide whether to treat it as nullable or non-null; misuse leads to runtime NPEs.
Sticky event
A one-shot event (like a toast) that is replayed when a StateFlow re-emits its latest value, causing it to reappear after rotation; solved by using a Channel instead.
MVI (Model-View-Intent)
An architecture where UI sends intents to a ViewModel, which updates a state object, and UI renders solely from that state, creating a unidirectional data flow that makes behavior more predictable.
StateFlow
A Kotlin coroutine-based state holder that emits the latest value to collectors and replays it to new collectors, used for persistent UI state in MVI.
Channel
A coroutine-based hot stream for one-shot events; events are consumed once and not replayed, avoiding sticky-event problems.
sealed class
A class hierarchy representing a restricted set of types; useful for modeling mutually exclusive states but doesn't support copy(), which complicates state updates.
From the discussion
Featured comments
無表情

Isn't it all just Kotlin + MVVM anyway?

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗