Skip to content

Why Mutekt?

Shreyas Patil edited this page Mar 22, 2023 · 2 revisions

Let's understand the reason for which Mutekt came to the picture.

Taking inspiration from Redux's way of state management having an immutable state model in the Kotlin ecosystem, implementation in Kotlin needs a lot of care and boilerplate to properly handle the state.

Without Mutekt

Assume this is the UI state model:

data class NotesState(val isLoading: Boolean, val notes: List<String>, val error: String?)

Here are well-known popular opinionated approaches in the Kotlin community to implement a reducer pattern:

1. Copying State model

In this approach, a mutable state flow is created with the initial state. Whenever a state needs to be mutated, previous the state is used to calculate the next state i.e. it just copies the previous state.

class NotesViewModel: ViewModel() {
    private val _state = MutableStateFlow(NotesState(false, emptyList(), null))
    val state = _state.asStateFlow()
    
    fun loadNotes() {
        _state.update { it.copy(isLoading = true) }
        
        val notes = listOf("Lorem Ipsum")
        _state.update { it.copy(notes = notes, isLoading = false) }
    }
}

In this approach, the following things need to be taken care of:

  • The new state should be updated atomically and with synchronization otherwise, state inconsistency will occur (i.e. update{} method of StateFlow).
  • By dev mistake, while updating a new state if the previous state is not copied (by it.copy()) the previous state will be lost.

2. Combining multiple states to form a new one

class NotesViewModel: ViewModel() {
    private val isLoading = MutableStateFlow(false)
    private val notes = MutableStateFlow(emptyList<String>())
    private val error = MutableStateFlow<String?>(null)
    
    val state: StateFlow<NotesState> = combine(isLoading, notes, error) { isLoading, notes, error ->
        NotesState(isLoading, notes, error)
    }.stateIn(viewModelScope, WhileSubscribed(), NotesState(false, emptyList(), null))
    
    fun loadNotes() {
        isLoading.value = true
        
        notes.value = listOf("Lorem Ipsum")
        isLoading.value = false
    }
}

In this approach, there's no scope for mistakes but needs repeated boilerplate for each property of the state model. As new state property is added in the codebase while development, refactoring is needed every time to have proper state management.

With Mutekt

Mutekt solves the issues around the above-mentioned approaches and lets developers focus on the state manipulation instead of declaring each and every state field every time by generating required and common boilerplate at compile time by annotation processing.

Just by inspiring from very popular Immer (from JS world), it simplifies writing immutable updates with the "mutating" syntax that helps writing clean reducer implementations.

With Mutekt you just need to declare the state model as the interface and apply the annotation. Rest magic is done by the KSP (As you already saw example earlier).

class NotesViewModel: ViewModel() {
-    private val isLoading = MutableStateFlow(false)
-    private val notes = MutableStateFlow(emptyList<String>())
-    private val error = MutableStateFlow<String?>(null)
    
-    val state: StateFlow<NotesState> = combine(isLoading, notes, error) { isLoading, notes, error ->
-        NotesState(isLoading, notes, error)
-    }.stateIn(viewModelScope, WhileSubscribed(), NotesState(false, emptyList(), null))

+    private val _state = MutableNotesState(false, emptyList(), null)
+    val state = _state.asStateFlow()

    fun loadNotes() {
-        isLoading.value = true
+        _state.isLoading = true
        
-        notes.value = getNotes()
-        isLoading.value = false
+        _state.apply {
+            isLoading = false
+            notes = listOf("Lorem Ipsum")
+        }
    }
}

Refer to this Wiki to know what code is generated under the hood by Mutekt.