MVI Architecture Migration

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

  1. Review and align on the proposal

  2. Select the pilot fragment

  3. Create an MVI template

  4. Begin the migration