<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged mvi</title>
<link>https://coderlegion.com/tag/mvi</link>
<description>Powered by Question2Answer</description>
<item>
<title>Stop Fighting Your State. Reduce And Conquer It.</title>
<link>https://coderlegion.com/18130/stop-fighting-your-state-reduce-and-conquer-it</link>
<description>&lt;p&gt;&lt;strong&gt;MVVM&lt;/strong&gt; seemed simple. Then you added ten MutableStateFlow properties to your ViewModel. &lt;strong&gt;MVI&lt;/strong&gt; promised purity. Then you wrote a middleware for side effects.&lt;/p&gt;
&lt;p&gt;There’s a better way.&lt;/p&gt;
&lt;h2 id=&quot;the-problem-with-mvvm&quot;&gt;The Problem With MVVM&lt;/h2&gt;
&lt;p&gt;A typical ViewModel looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class ProfileViewModel : ViewModel() {
    val name = MutableStateFlow(&quot;&quot;)
    val email = MutableStateFlow(&quot;&quot;)
    val isLoading = MutableStateFlow(false)
    val error = MutableStateFlow&amp;lt;String?&amp;gt;(null)
    fun updateProfile(name: String) {
        viewModelScope.launch {
            isLoading.value = true
            try {
                profileService.update(name)
                *Emails are not allowed*e = name
            } catch (e: Exception) {
                error.value = e.message
            } finally {
                isLoading.value = false
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Five mutable properties. Loading state scattered across three places. Error handling duplicated in every method. And the ViewModel doesn’t own its side effects — &lt;code&gt;viewModelScope&lt;/code&gt; does.&lt;/p&gt;
&lt;h2 id=&quot;the-problem-with-mvi&quot;&gt;The Problem With MVI&lt;/h2&gt;
&lt;p&gt;MVI fixes the state explosion by putting everything in a sealed interface:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;sealed interface ProfileState {
    data object Loading : ProfileState
    data class Loaded(val name: String, val email: String) : ProfileState
    data class Error(val message: String) : ProfileState
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But MVI doesn’t tell you how to handle side effects. Some use middleware. Some use &lt;code&gt;Channel&lt;/code&gt;. Some hack it with &lt;code&gt;LaunchedEffect&lt;/code&gt;. Every project reinvents the wheel.&lt;/p&gt;
&lt;h2 id=&quot;reduce-amp-conquer&quot;&gt;Reduce &amp;amp; Conquer&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2820081858493314974&quot; alt=&quot;Reduce &amp;amp; Conquer layers&quot;&gt;&lt;/p&gt;
&lt;p&gt;The core idea: &lt;em&gt;a reducer is a pure function that returns more than just state&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class Transition&amp;lt;State, Event&amp;gt;(
    val state: State,
    val events: List&amp;lt;Event&amp;gt; = emptyList(),
    val effects: List&amp;lt;Effect&amp;gt; = emptyList()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A transition has three outputs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;State&lt;/strong&gt; - the new state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Events&lt;/strong&gt; - one-shot notifications (navigation, snackbar)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effects&lt;/strong&gt; - long-running or async work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Effects are first-class citizens:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;sealed interface Effect {
    data class Stream&amp;lt;Command&amp;gt;(
        val key: Any,
        val flow: Flow&amp;lt;Command&amp;gt;,
        val strategy: Strategy = Strategy.Sequential,
        val fallback: (suspend (Throwable) -&amp;gt; Command)? = null
    ) : Effect
    data class Action&amp;lt;Command&amp;gt;(
        val key: Any,
        val fallback: (suspend (Throwable) -&amp;gt; Command)? = null,
        val block: suspend () -&amp;gt; Command
    ) : Effect
    data class Cancel(val key: Any) : Effect
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stream&lt;/strong&gt; - subscribes to a flow, emits commands back&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Action&lt;/strong&gt; - runs one async operation, emits a command&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cancel&lt;/strong&gt; - cancels by key, preventing leaks&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;a-reducer-in-practice&quot;&gt;A Reducer in Practice&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class ProfileReducer(
    private val profileService: ProfileService
) : Reducer&amp;lt;ProfileState, ProfileCommand, ProfileEvent&amp;gt; {
    override fun reduce(
        state: ProfileState,
        command: ProfileCommand
    ): Transition&amp;lt;ProfileState, ProfileEvent&amp;gt; = when (command) {
        is ProfileCommand.UpdateProfile -&amp;gt; transition(
            state.copy(isLoading = true)
        ).effect(
            action(
                key = &quot;update_profile&quot;,
                fallback = { ProfileCommand.ProfileError(it) },
                block = {
                    profileService.update(command.name)
                    ProfileCommand.ProfileUpdated
                }
            )
        )
        is ProfileCommand.ProfileUpdated -&amp;gt; transition(
            state.copy(isLoading = false)
        ).event(ProfileEvent.NavigateBack)
        is ProfileCommand.ProfileError -&amp;gt; transition(
            state.copy(
                isLoading = false,
                error = command.throwable.message
            )
        )
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No &lt;code&gt;viewModelScope&lt;/code&gt;. No &lt;code&gt;LaunchedEffect&lt;/code&gt;. No mutable properties. One pure function.&lt;/p&gt;
&lt;h2 id=&quot;why-this-is-the-benchmark&quot;&gt;Why This Is The Benchmark&lt;/h2&gt;
&lt;h3 id=&quot;mvvm&quot;&gt;MVVM&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;State&lt;/strong&gt;: Multiple MutableStateFlow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Side effects&lt;/strong&gt;: viewModelScope.launch&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cancellation&lt;/strong&gt;: Manual&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing&lt;/strong&gt;: Mock ViewModel&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;mvi&quot;&gt;MVI&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;State&lt;/strong&gt;: Single sealed class/interface&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Side effects&lt;/strong&gt;: Ad-hoc (middleware, Channel)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cancellation&lt;/strong&gt;: Manual&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing&lt;/strong&gt;: Mock reducer + middleware&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;reduce-amp-conquer-1&quot;&gt;Reduce &amp;amp; Conquer&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;State&lt;/strong&gt;: Single sealed interface&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Side effects&lt;/strong&gt;: Built-in (Effect)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cancellation&lt;/strong&gt;: Automatic by key&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing&lt;/strong&gt;: One pure function call&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Testing a reducer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Test
fun `update profile sets loading and fires effect`() {
    val transition = reducer.reduce(
        state = ProfileState(),
        command = ProfileCommand.UpdateProfile(&quot;Alice&quot;)
    )
    assertTrue(transition.state.isLoading)
    assertEquals(1, transition.effects.size)
    assertEquals(0, transition.events.size)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One call. No coroutines. No mocks.&lt;/p&gt;
&lt;h2 id=&quot;the-rule&quot;&gt;The Rule&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;State flows down. Commands flow up. Effects manage the rest.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The View sends Commands. The Reducer returns a Transition with new State, Events, and Effects. The Feature executes Effects and feeds resulting Commands back into the Reducer. That's it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16113002570422559213&quot; alt=&quot;Reduce &amp;amp; Conquer diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;Full implementation in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.github.numq/reduce-and-conquer&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18130/stop-fighting-your-state-reduce-and-conquer-it</guid>
<pubDate>Sun, 17 May 2026 04:47:30 +0000</pubDate>
</item>
</channel>
</rss>