Why MVI?
Current Issues
-
Business logic is mixed with UI code, making it hard to test
-
State is scattered across Fragment fields and lost on configuration changes
-
Tight coupling between components reduces maintainability
Proposed Solution: MVI Architecture
What is MVI?
Intent → Model → State → View
-
Intent: User actions such as clicks or text input
-
Model: Processes intents and produces a new state
-
State: Immutable data representing the single source of truth
-
View: Observes state changes and updates the UI
Unidirectional data flow = predictable and testable UI
Key Advantages
1. Testability
// Before: Hard to test (UI and logic tightly coupled)
fragment.performSearch(); // Requires Android instrumentation tests
// After: Easy to test (pure business logic)
reduce(SearchIntent("query"), State()); // Simple unit test
2. State Management
// Before: State stored in Fragment fields (lost on rotation)
private List<FeedItem> episodes = new ArrayList<>();
// After: Immutable State object (survives rotation)
data class State(
val episodes: List<FeedItem> = emptyList()
)
3. Predictable Data Flow
-
One-way data flow simplifies debugging
-
No circular dependencies
-
Clear lifecycle: Intent → Model → State → View
4. Consistent Error Handling
// Errors are part of the State
data class State(
val isLoading: Boolean = false,
val error: String? = null
)
5. Maintainability
-
Clear separation between UI and business logic
-
Easier to add new features
-
Cleaner and more effective code reviews
Migration Plan
Phase 1: Proof of Concept (2–3 weeks)
-
Migrate one fragment (e.g.,
AddFeedFragment) -
Define a reusable MVI template
-
Document the approach and best practices
Phase 2: Core Features (1–2 months)
- Migrate core fragments (Queue, Episodes, FeedList)
Phase 3: Complete Migration (2–3 months)
-
Migrate remaining fragments
-
Update and expand test coverage
-
Finalize documentation
Example: Before vs After
Before (Current Approach)
public class AddFeedFragment extends Fragment {
private void performSearch() {
String query = editText.getText().toString();
if (query.matches("http[s]?://.*")) {
addUrl(query); // UI and logic mixed
} else {
activity.loadChildFragment(...);
}
}
}
After (MVI Approach)
// Intent
sealed class Intent {
data class SearchIntent(val query: String) : Intent()
}
// State
data class State(
val navigationTarget: NavigationTarget? = null
)
// Reducer (pure function — easy to test)
fun reduce(intent: Intent, state: State): State {
return when (intent) {
is SearchIntent -> state.copy(
navigationTarget = if (intent.query.matches(...))
NavigationTarget.AddUrl(intent.query)
else
NavigationTarget.Search(intent.query)
)
}
}
// Fragment (only observes state)
viewModel.state.collect { state ->
state.navigationTarget?.let { navigate(it) }
}
Benefits at a Glance
| Benefit | Impact |
|---|---|
| Testability | Business logic is fully unit-testable |
| State Management | Immutable and lifecycle-safe |
| Maintainability | Clear separation of concerns |
| Debugging | Predictable, one-way data flow |
Recommendation
Start small: Migrate AddFeedFragment as a proof of concept
-
Low risk
-
Validates the architecture
-
Establishes reusable patterns
-
Builds team confidence
Gradually migrate additional fragments based on priority and impact.
Next Steps
-
Review and align on the proposal
-
Select the pilot fragment
-
Create an MVI template
-
Begin the migration