<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Emily's Blog]]></title><description><![CDATA[Emily's Blog]]></description><link>https://blog.emilym.cl</link><generator>RSS for Node</generator><lastBuildDate>Fri, 22 May 2026 10:10:47 GMT</lastBuildDate><atom:link href="https://blog.emilym.cl/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Android ViewModel State Machine]]></title><description><![CDATA[Are you faced with a UI best represented by a state machine, but for some reason every other tutorial you can find is terrible and only tells you how to make a generic FSM (or even just calls a sealed interface representing state a FSM), something yo...]]></description><link>https://blog.emilym.cl/android-viewmodel-state-machine</link><guid isPermaLink="true">https://blog.emilym.cl/android-viewmodel-state-machine</guid><category><![CDATA[Android]]></category><category><![CDATA[ViewModel]]></category><dc:creator><![CDATA[Emily McLean]]></dc:creator><pubDate>Thu, 27 Jun 2024 03:59:55 GMT</pubDate><content:encoded><![CDATA[<p>Are you faced with a UI best represented by a state machine, but for some reason every other tutorial you can find is terrible and only tells you how to make a generic FSM (or even just calls a sealed interface <em>representing state</em> a FSM), something you obviously already know? Well I was, and it has only made me hate Medium more. I came up with a fairly workable solution that mostly respects the ViewModel pattern, and I am going to document it here.</p>
<p>My key abstraction is to represent interaction with the ViewModel as a series of actions and events passed to and from the State. This is based on my experience working with complicated Jetpack Compose widgets (future me: insert a link if I ever write an article about that). Here is an example implementation of the base state machine:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StateMachine</span>&lt;<span class="hljs-type">Event, Action</span>&gt;</span>(
    callback: ActionCallback&lt;Action&gt;
) {

    <span class="hljs-keyword">val</span> callback = WeakReference(callback)
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> _current: State&lt;Event, Action&gt;? = <span class="hljs-literal">null</span>
    <span class="hljs-keyword">val</span> current: State&lt;Event, Action&gt;? <span class="hljs-keyword">get</span>() = _current

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">transition</span><span class="hljs-params">(state: <span class="hljs-type">State</span>&lt;<span class="hljs-type">Event</span>, Action&gt;)</span></span> {
        _current?.apply {
            teardown()
            callback = <span class="hljs-literal">null</span>
        }
        _current = <span class="hljs-literal">null</span>

        state.callback = callback
        state.setup()
        _current = state
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">process</span><span class="hljs-params">(event: <span class="hljs-type">Event</span>)</span></span> {
        _current?.process(event) ?: run {
            Log.w(<span class="hljs-string">"StateMachine"</span>, <span class="hljs-string">"Event dropped because there is not state to consume it"</span>)
        }
    }

}

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ActionCallback</span>&lt;<span class="hljs-type">Action</span>&gt; </span>{

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">action</span><span class="hljs-params">(action: <span class="hljs-type">Action</span>)</span></span>

}

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">State</span>&lt;<span class="hljs-type">Event, Action</span>&gt; </span>{

    <span class="hljs-keyword">var</span> callback: WeakReference&lt;ActionCallback&lt;Action&gt;&gt;?

    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span></span> {}
    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">process</span><span class="hljs-params">(event: <span class="hljs-type">Event</span>)</span></span> {}
    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">teardown</span><span class="hljs-params">()</span></span> {}

}

<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AbstractState</span>&lt;<span class="hljs-type">Event, Action</span>&gt;: <span class="hljs-type">State</span>&lt;<span class="hljs-type">Event, Action</span>&gt; </span>{

    <span class="hljs-keyword">override</span> <span class="hljs-keyword">var</span> callback: WeakReference&lt;ActionCallback&lt;Action&gt;&gt;? = <span class="hljs-literal">null</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _stateScope = <span class="hljs-keyword">object</span> : CoroutineScope {
        <span class="hljs-keyword">override</span> <span class="hljs-keyword">val</span> coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
        <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">cancel</span><span class="hljs-params">()</span></span> {
            coroutineContext.cancel()
        }
    }
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">val</span> stateScope: CoroutineScope <span class="hljs-keyword">get</span>() = _stateScope

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">action</span><span class="hljs-params">(<span class="hljs-keyword">vararg</span> triggers: <span class="hljs-type">Action</span>)</span></span> {
        callback?.<span class="hljs-keyword">get</span>()?.let { callback -&gt;
            <span class="hljs-keyword">for</span> (trigger <span class="hljs-keyword">in</span> triggers) {
                callback.action(trigger)
            }
        } ?: run {
            Log.e(<span class="hljs-string">"StateMachine"</span>, <span class="hljs-string">"No callback available!"</span>)
        }
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">teardown</span><span class="hljs-params">()</span></span> {
        _stateScope.cancel()
        <span class="hljs-keyword">super</span>.teardown()
    }

}
</code></pre>
<p>Now, there are a couple things I'm not happy about this implementation, primarily the ActionCallback interface. It would be far more <em>Kotlin-y</em> and <em>Reactive Programming-y</em> if instead there was a Flow or something to which was subscribed. I'll leave that as an exercise for the reader.</p>
<p>Before we start writing states, it's a good idea to define the ways in which our state machine will interact with the outside world using Events and Actions. As you may have gathered, Events are passed to the State, while Actions are passed back to tell the ViewModel to do something. Let's make a basic one:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">sealed</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ExampleEvent</span> </span>{
    <span class="hljs-keyword">object</span> OpenBrowseButtonClicked: ExampleEvent
    <span class="hljs-keyword">object</span> OpenSearchButtonClicked: ExampleEvent
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SearchTextChanged</span>: <span class="hljs-type">ExampleEvent</span></span>
}

<span class="hljs-keyword">sealed</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ExampleAction</span> </span>{
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Transition</span></span>(<span class="hljs-keyword">val</span> state: Class&lt;*&gt;): ExampleAction
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ShowLoadingState</span></span>(<span class="hljs-keyword">val</span> show: <span class="hljs-built_in">Boolean</span>): ExampleAction
    <span class="hljs-comment">// Null here means "hide"</span>
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ShowErrorState</span></span>(<span class="hljs-keyword">val</span> exception: Exception?): ExampleAction
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PresentItems</span></span>(<span class="hljs-keyword">val</span> results: List&lt;String&gt;): ExampleAction
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ShowSaveSearchButton</span></span>(<span class="hljs-keyword">val</span> show: <span class="hljs-built_in">Boolean</span>): ExampleAction
}
</code></pre>
<p>After we've done that, we can actually start writing our states!</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BrowseExampleState</span></span>(
    <span class="hljs-keyword">val</span> browseRepository: BrowseRepository
): AbstractState&lt;ExampleEvent, ExampleAction&gt;() {

    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">super</span>.setup()
        load()
    }

    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">process</span><span class="hljs-params">(event: <span class="hljs-type">ExampleEvent</span>)</span></span> {
        <span class="hljs-keyword">super</span>.process(event)
        <span class="hljs-keyword">when</span> (event) {
            <span class="hljs-keyword">is</span> ExampleEvent.OpenSearchButtonClicked -&gt; {
                action(Transition(SearchExampleState::<span class="hljs-keyword">class</span>.java)))
            }
            <span class="hljs-keyword">else</span> -&gt; {}
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">load</span><span class="hljs-params">()</span></span> {
        action(ExampleAction.ShowLoadingState(<span class="hljs-literal">true</span>))
        stateScope.launch {
            <span class="hljs-keyword">try</span> {
                action(
                    ExampleAction.PresentItems(
                        ExampleAction.browseRepository.load()
                    )
                )
                action(ExampleAction.ShowLoadingState(<span class="hljs-literal">false</span>))
            } <span class="hljs-keyword">catch</span>(e: Exception) {
                action(
                    ExampleAction.ShowErrorState(
                        e
                    )
                )
                action(ExampleAction.ShowLoadingState(<span class="hljs-literal">false</span>))
            }
        }
    }

} 

...
</code></pre>
<p>Wow that was easy! Next we need to actually hook this all up to a ViewModel, which is similarly simple.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExampleViewModel</span></span>(
    <span class="hljs-keyword">val</span> browseRepository: BrowseRepository
): ViewModel(), ActionCallback&lt;ExampleAction&gt; {

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> stateMachine = StateMachine&lt;ExampleEvent, ExampleAction&gt;(<span class="hljs-keyword">this</span>)

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span></span> {
        transition(BrowseExampleState::<span class="hljs-keyword">class</span>.java)
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">transition</span><span class="hljs-params">(state: <span class="hljs-type">Class</span>&lt;*&gt;)</span></span> {
        stateMachine.transition(
            <span class="hljs-keyword">when</span>(state) {
                <span class="hljs-keyword">is</span> BrowseExampleState::<span class="hljs-keyword">class</span>.java -&gt; BrowseExampleState(
                    browseRepository
                )
            } <span class="hljs-keyword">as</span> State&lt;ExampleEvent, ExampleAction&gt;
        )
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">action</span><span class="hljs-params">(action: <span class="hljs-type">ExampleAction</span>)</span></span> {
        <span class="hljs-keyword">when</span> (action) {
            <span class="hljs-keyword">is</span> ExampleAction.Transition -&gt; transition(action.state)
            <span class="hljs-keyword">else</span> -&gt; { <span class="hljs-comment">/* Implement more handlers here */</span> }
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">openSearchButtonClicked</span><span class="hljs-params">()</span></span> {
        stateMachine.process(ExampleEvent.OpenSearchButtonClicked)
    }

}
</code></pre>
<p>Wow that was pretty neat! I hope you learned something from this tutorial. Feel free to tell me how this can be improved because I'm sure it can be.</p>
<p><em>La Fin</em></p>
]]></content:encoded></item></channel></rss>