This repository contains the article describing my attempt to implement a simple state reducer based on Kotlin Flow and an example app that uses it.

Overview

All you need for MVI is Kotlin. How to reduce without a reducer?

πŸ“œ Description

This repository contains the article describing my attempt to implement a simple state reducer based on Kotlin Flow and an example app that uses it.

πŸ’‘ Motivation and Context

Like any Android developer following the latest trends, I like MVI architecture and the unidirectional data flow concept. It solves many issues out of the box making our code even more bulletproof.

Alt Text

In this article, I won't go into detail about what MVI is, but you can find many great write-ups about it, e.g.

Playing with libraries like MVICore, Mobius, or Orbit inspired me to experiment and try to implement a flow that can perform state reduction.

That's how StateReducerFlow was born. Let me explain how I've built it, how it works, and how you can use it.

πŸ‘¨β€πŸŽ“ Thinking process

Please keep in mind that the following examples are simplified.

Let's start with a simple counter. It has one state that can be changed with two events: decrement and increment.

sealed class Event {
    object Increment : Event()
    object Decrement : Event()
}

data class State(
    val counter: Int = 0
)

class ViewModel {
    val state = MutableStateFlow(State())

    fun handleEvent(event: Event) {
        when (event) {
            is Increment -> state.update { it.copy(counter = it.counter + 1) }
            is Decrement -> state.update { it.copy(counter = it.counter - 1) }
        }
    }
}

Using the above approach, we can structure our logic in the following way:

Event -> ViewModel -> State

One issue, though, is that handleEvent can be called from any thread. Having unstructured state updates can lead to tricky bugs and race conditions. Luckily, state.update() is already thread-safe, but still, any other logic can be affected.

To solve that we can introduce a channel that will allow us to process events sequentially, no matter from which thread they come.

class ViewModel {

    private val events = Channel()

    val state = MutableStateFlow(State())

    init {
        events.receiveAsFlow()
            .onEach(::updateState)
            .launchIn(viewModelScope)
    }

    fun handleEvent(event: Event) {
        events.trySend(event)
    }

    private fun updateState(event: Event) {
        when (event) {
            is Increment -> state.update { it.copy(counter = it.counter + 1) }
            is Decrement -> state.update { it.copy(counter = it.counter - 1) }
        }
    }
}

Much better. Now we process all events sequentially but state updates are still possible outside of the updateState method. Ideally, state updates should be only allowed during event processing.

To achieve that we can implement a simple reducer using runningFold.

class ViewModel {

    private val events = Channel()

    val state = events.receiveAsFlow()
        .runningFold(State(), ::reduceState)
        .stateIn(viewModelScope, Eagerly, State())

    fun handleEvent(event: Event) {
        events.trySend(event)
    }

    private fun reduceState(currentState: State, event: Event): State {
        return when (event) {
            is Increment -> currentState.copy(counter = currentState.counter + 1)
            is Decrement -> currentState.copy(counter = currentState.counter - 1)
        }
    }
}

Now only the reduceState method can perform state transformations.

Alt Text

When you look at this example ViewModel you may notice that only the reduceState method contains important logic. Everything else is just boilerplate that needs to be repeated for every new ViewModel.

As we all like to stay DRY, I needed to extract the generic logic from the ViewModel.

That's how StateReducerFlow was born.

πŸš€ StateReducerFlow

I wanted StateReducerFlow to be a StateFlow that can handle generic events. I started with this definition:

interface StateReducerFlow : StateFlow {
    fun handleEvent(event: EVENT)
}

Moving forward I extracted my ViewModel logic to the new flow implementation:

private class StateReducerFlowImpl(
    initialState: STATE,
    reduceState: (STATE, EVENT) -> STATE,
    scope: CoroutineScope
) : StateReducerFlow {

    private val events = Channel()

    private val stateFlow = events
        .receiveAsFlow()
        .runningFold(initialState, reduceState)
        .stateIn(scope, Eagerly, initialState)

    override val replayCache get() = stateFlow.replayCache

    override val value get() = stateFlow.value

    override suspend fun collect(collector: FlowCollector): Nothing {
        stateFlow.collect(collector)
    }

    override fun handleEvent(event: EVENT) {
        events.trySend(event)
    }
}

As you can see, the only new things are a few overrides from StateFlow. To construct the flow you provide the initial state, the function that can reduce it, and the coroutine scope in which the state can be shared.

The last missing part is a factory function that can create our new flow. I've decided to go with ViewModel extension to access viewModelScope.

fun  ViewModel.StateReducerFlow(
    initialState: STATE,
    reduceState: (STATE, EVENT) -> STATE,
): StateReducerFlow = StateReducerFlowImpl(initialState, reduceState, viewModelScope)

Now we can migrate our ViewModel to the new StateReducerFlow.

class ViewModel {

    val state = StateReducerFlow(
        initialState = State(),
        reduceState = ::reduceState
    )

    private fun reduceState(currentState: State, event: Event): State {
        return when (event) {
            is Increment -> currentState.copy(counter = currentState.counter + 1)
            is Decrement -> currentState.copy(counter = currentState.counter - 1)
        }
    }
}

VoilΓ ! The boilerplate is gone.

Alt Text

Now anyone who has access to StateReducerFlow can send events to it, e.g.

class ExampleActivity : Activity() {

    private val viewModel = ViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.state.handleEvent(ExampleEvent)
    }
}

That's it! Are you interested in how it works in a real app or how it can be tested? See my example project: https://github.com/linean/StateReducerFlow/tree/main/app/src

Stay inspired!

You might also like...
Crunch-Mobile - A Food Delivery Mobile App which uses Modern App Architecture Pattern, Firebase And a Simple Restful Api
Crunch-Mobile - A Food Delivery Mobile App which uses Modern App Architecture Pattern, Firebase And a Simple Restful Api

Crunch-Mobile This is a Food Delivery Mobile App which uses Modern App Architect

ConstraintSetChangesTest - Simple project showing Changes of ConstraintSet value as part of mutable state in JetpackCompose.

ConstraintSetChangesTest Simple project showing Changes of ConstraintSet value as part of mutable state in JetpackCompose. Version: implementation

πŸͺ Modern Android development with Hilt, Coroutines, Flow, JetPack(ViewModel) based on MVVM architecture.

Ceres πŸͺ Modern Android development with Hilt, Coroutines, Flow, JetPack(ViewModel) based on MVVM architecture. Download Gradle Add the dependency bel

Netflix inspired OTT Home Screen, Contains implementation in Reactjs, Kotlin React Wrapper, Jetpack Compose Web

Netflix-Clone-React Practising React by building Netflix Clone Requirements TMDB api key : Add TMDB API key to AppApi.kt Learning Resourcce Build Netf

Kotlin coroutine capable Finite-State Machine (multiplatform)
Kotlin coroutine capable Finite-State Machine (multiplatform)

Comachine Features Kotlin corutines. Event handlers can launch coroutines for collecting external events of performing side effects. Structured concur

Kotlin coroutine capable Finite-State Machine (multiplatform)
Kotlin coroutine capable Finite-State Machine (multiplatform)

Comachine Features Kotlin corutines. Event handlers can launch coroutines for collecting external events of performing side effects. Structured concur

Stresscraft - State-of-art Minecraft stressing software written in Kotlin

StressCraft (W.I.P) State-of-art Minecraft stressing software written in Kotlin.

ScopedState - Android Scoped State With Kotlin

Android Scoped State There is no need for complicated code - just define scopes

πŸ’« Small microservice to handle state changes of Kubernetes pods and post them to Instatus or Statuspages

πŸ’« Kanata Small microservice to handle state changes of Kubernetes pods and post them to Instatus or Statuspages πŸ€” Why? I don't really want to implem

Owner
Maciej Sady
Maciej Sady
Basic app to use different type of observables StateFlow, Flow, SharedFlow, LiveData, State, Channel...

stateflow-flow-sharedflow-livedata Basic app to use different type of observables StateFlow, Flow, SharedFlow, LiveData, State, Channel... StateFlow,

Raheem 5 Dec 21, 2022
This repository contains the source code for the PokeApi Android app.

PokeApi App This repository contains the source code for the PokeApi Android app.

Nilton HuamanΓ­ Carlos 0 Nov 4, 2021
An attempt to create the opensource clone for the Calibre APP in Kotlin multiplatform

CalibreKMM An attempt/research clone for calibre app written in kotlin multiplatform. Plan to target Desktop JVM with (Jetpack Compose) TODOs Koin doe

Anmol Verma 3 Jun 15, 2022
This Project for how to use MVVM , state flow, Retrofit, dagger hit, coroutine , use cases with Clean architecture.

Clean-architecture This Project for how to use MVVM , state flow, Retrofit, dagger hit, coroutine , use cases with Clean architecture. Why i should us

Kareem Aboelatta 10 Dec 13, 2022
Shoe Store project first Attempt

Shoe Store project first Attempt User Info: email: [email protected] password:12345 I had problem to select the home Screen for the navigation gr

null 0 Nov 25, 2021
Android project setup files when developing apps from scratch. The codebase uses lates jetpack libraries and MVVM repository architecture for setting up high performance apps

Android architecture app Includes the following Android Respository architecture MVVM Jepack libraries Carousel view Kotlin Kotlin Flow and Livedata P

null 2 Mar 31, 2022
[Android-Kotlin] MVVM, ViewModel, LiveData, Observer, DataBinding, Repository, Retrofit, Dagger example

SimpleMvvmDaggerKotlin [Android-Kotlin] MVVM, ViewModel, LiveData, Observer, DataBinding, Repository, Retrofit, Dagger example [Image1 : User informat

DONGGEUN JUNG 2 Oct 24, 2022
RoomJetpackCompose is an app written in Kotlin and shows a simple solution to perform CRUD operations in the Room database using Kotlin Flow in clean architecture.

RoomJetpackCompose is an app written in Kotlin and shows a simple solution to perform CRUD operations in the Room database using Kotlin Flow in clean architecture.

Alex 27 Jan 1, 2023
Simple State Machines in Kotlin (KSSM)

Simple State Machines in Kotlin (KSSM) What is this? KSSM (reordered: Kotlin - Simple State Machines) provides an easy and simple DSL (Domain Specific

Milos Marinkovic 22 Dec 12, 2022
Extension functions over Android's callback-based APIs which allows writing them in a sequential way within coroutines or observe multiple callbacks through kotlin flow.

callback-ktx A lightweight Android library that wraps Android's callback-based APIs into suspending extension functions which allow writing them in a

Sagar Viradiya 171 Oct 31, 2022