https://kotlinlang.org logo
#compose
Title
# compose
n

Norbi

06/05/2022, 2:43 PM
I would like to propose a "new" kind of Compose state management instead of Redux-like solutions.
Sample Compose/Web application: https://kotlinw.github.io/kotlinw/immutator-example-webapp/index.html 1. First you define your application's domain model as interfaces.
Copy code
@Immutate
interface TodoAppState {
    val screenStack: List<Screen>
    val todoLists: List<TodoList>
}

@Immutate
interface EditScreen : Screen {
    val editedTodoListId: Long
}

@Immutate
interface TodoList {
    val id: Long
    val name: String
    val items: List<TodoItem>
}

@Immutate
interface TodoItem {
    val text: String
    val isCompleted: Boolean
}

...
See: https://github.com/kotlinw/kotlinw/blob/main/kotlinw-immutator-example-webapp/src/jsMain/kotlin/kotlinw/immutator/example/webapp/State.kt 2. A KSP processor will generate the corresponding immutable and mutable classes from the interface declarations. Eg. interface
TodoItem
will trigger the generation of: •
data class TodoItemImmutable
interface TodoItemMutable
class TodoItemMutableImpl
• some useful extension methods like
TodoItemImmutable.toMutable()
and
TodoItemMutable.toImmutable()
Converting between immutable and mutable objects is efficient, eg.
immutable.toMutable().toImmutable() === immutable
. 3. A state store is defined holding the current immutable state ("single source of truth"):
Copy code
val stateFlow = MutableStateFlow(createInitialState())

inline fun mutateState(mutator: (TodoAppStateMutable) -> Unit) = stateFlow.update { it.mutate(mutator) }

private fun createInitialState() = TodoAppStateImmutable(...)
See: https://github.com/kotlinw/kotlinw/blob/main/kotlinw-immutator-example-webapp/src/jsMain/kotlin/kotlinw/immutator/example/webapp/StateStore.kt 4. The application is composed using the immutable model, eg.
Copy code
@Composable
fun TodoListView(todoList: TodoListImmutable, isCompletedShown: Boolean) {
    Div {
        H3 {
            Text(todoList.name)
        }
        Div {
            val items by derivedStateOf { todoList.items.filter { if (isCompletedShown) true else it.isActive } }
            items.forEach {
                key(it) {
                    Div {
                        if (isCompletedShown) {
                            if (it.isCompleted) Text("☑ ") else Text("☐ ")
                        } else {
                            Text("• ")
                        }
                        Text(it.text)
                    }
                }
            }
        }
    }
}
5. State is mutated in a controlled manner using state operations (like "actions" in case of Javascript-based state stores).
Copy code
fun TodoAppStateMutable.addNewTodoList() {
    todoLists.add(TodoListImmutable(System.now().toEpochMilliseconds(), "", persistentListOf()).toMutable())
}

...

Button({
	onClick { mutateState { it.addNewTodoList() } }
}) {
	Text("Add new TODO list")
}
			
...

Button({
	onClick {
		mutateState {
			it.getTodoListItems().add(TodoItemImmutable("", false).toMutable())
		}
	}
}) {
	Text("Add item")
}
Sources: https://github.com/kotlinw/kotlinw/tree/main/kotlinw-immutator-example-webapp (Please note: this is a demo application, some Compose best practices are not applied to make the concepts more clear.)
a

Arjan van Wieringen

06/05/2022, 7:51 PM
Hello, interesting. But to be honest I am unclear what this would bring me? Isn't it so that I can do everything you've made with State and MutableState? Maybe you can enlighten me what the benefits are compared to the “classic” solutions?
n

Norbi

06/05/2022, 9:37 PM
I think this architecture is very similar to Redux's, so it is a centralized state management approach similar to redux-kotlin that has a nice description:
... a project that strictly follows the principles and patterns it can be a very productive and testable pattern.
In general, use Redux when you have reasonable amounts of data changing over time, you need a single source of truth.
Beside the same advantages, - it is more Kotlin-ish in my opinion (state is modified by calling functions instead of the less natural "action dispatching") - results in less error-prone, efficient Compose code (because at most places you work with immutable data structures, data modification must be "guarded by"
mutateState { ... }
calls) - the automatic generation of boilerplate code makes the state model definition fairly readable - and the usage of immutable data makes the code more safe (uncontrolled state modification is not possible).
1
c

Casey Brooks

06/05/2022, 10:07 PM
Looks nice! I like that it makes Compose-specific models, definitely a nice thing for people like me who haven't quite gotten the hang of the Compose immutability annotations. It also reminds me Arrow Lenses https://arrow-kt.io/docs/optics/lens/
z

Zach Klippenstein (he/him) [MOD]

06/06/2022, 12:52 AM
Haha I love this! I built a prototype of something very similar a while back. One note though, the
@Immutable
annotations on the interfaces are incorrect if you've got a mutable implementation. You want
@Stable
instead.
n

Norbi

06/06/2022, 5:58 AM
Those are
@Immutate
, for the KSP processor 😉
t

Tash

06/06/2022, 5:25 PM
This is super interesting. In this step “A KSP processor will generate the corresponding immutable and mutable classes from the interface declarations.” how is the “mutable” part represented? Using Snapshot State? We’re currently using a lightweight redux-kotlin port for immutable “LCE” states , but it does not include any support for Snapshot State, and I’ve been trying to think about how/if it can be leveraged for cases of
mutableStateOf
fields (needed for cases of frequently changing state).
n

Norbi

06/06/2022, 6:43 PM
how is the “mutable” part represented
It is represented by a simple "property name" - "property value" map, accessed by delegated properties. The difficult part of the job is handled by internal implementation classes, like the
MutableObjectState
class. Eg., for the example webapp's
TodoItem
interface the following code is generated (nothing special, as I mentioned, almost all functionality is implemented as a library to minimize the complexity of generated code):
Copy code
package kotlinw.immutator.example.webapp

import androidx.compose.runtime.Immutable
import kotlinw.immutator.internal.ImmutableObject
import kotlinw.immutator.internal.MutableObjectImplementor
import kotlinw.immutator.internal.MutableObjectState
import kotlinw.immutator.internal.mutableValueProperty
import kotlin.reflect.KProperty1

fun TodoItem.toMutable(): TodoItemMutable {
    return when (this) {
        is TodoItemMutable -> this
        is TodoItemImmutable -> _immutator_convertToMutable()
        else -> throw IllegalStateException()
    }
}

fun TodoItem.toImmutable(): TodoItemImmutable {
    return when (this) {
        is TodoItemImmutable -> this
        is TodoItemMutable -> (this as TodoItemMutableImpl)._immutator_convertToImmutable()
        else -> throw IllegalStateException()
    }
}

interface TodoItemMutable : TodoItem,
    MutableObjectImplementor<TodoItemMutable, TodoItemImmutable> {
    override var text: String

    override var isCompleted: Boolean
}

@Immutable
data class TodoItemImmutable(
    override val text: String,
    override val isCompleted: Boolean,
) : TodoItem, ImmutableObject<TodoItemMutable> {
    override fun _immutator_convertToMutable(): TodoItemMutable = TodoItemMutableImpl(this)

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || other !is TodoItem) {
            return false
        }

        if (text != other.text) {
            return false
        }
        if (isCompleted != other.isCompleted) {
            return false
        }

        return true
    }

    override fun hashCode(): Int {
        var result = text.hashCode()
        result = 31 * result + (isCompleted.hashCode())
        return result
    }

    override fun toString(): String = """TodoItem(text=$text, isCompleted=$isCompleted)"""
}

class TodoItemMutableImpl(
    private val source: TodoItemImmutable,
) : TodoItemMutable {
    private val _objectState: MutableObjectState<TodoItemMutable, TodoItemImmutable> =
        MutableObjectState(
            source,
            properties
        )

    override val _immutator_isModified: Boolean
        get() = _objectState.isModified

    override var text: String by mutableValueProperty(_objectState)

    override var isCompleted: Boolean by mutableValueProperty(_objectState)

    override fun _immutator_convertToImmutable(): TodoItemImmutable =
        if (_immutator_isModified) {
            TodoItemImmutable(text, isCompleted)
        } else {
            source
        }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || other !is TodoItem) {
            return false
        }

        if (text != other.text) {
            return false
        }
        if (isCompleted != other.isCompleted) {
            return false
        }

        return true
    }

    override fun hashCode(): Int {
        var result = text.hashCode()
        result = 31 * result + (isCompleted.hashCode())
        return result
    }

    override fun toString(): String = """TodoItem(text=$text, isCompleted=$isCompleted)"""

    companion object {
        val properties: Map<String, KProperty1<TodoItemImmutable, *>> = listOf(
            TodoItemImmutable::text, TodoItemImmutable::isCompleted
        ).associateBy { it.name }
    }
}
Please note that this is more of a proof of concept than a usable library. But I plan to continue its development, and I already use it in an internal Compose/Desktop project successfully.
how is the “mutable” part represented? Using Snapshot State?
@Tash, it took a while to understand your question - sorry 🙂 Compose knows nothing about the mutable variant of the state model. Composables always work with immutable data, ie.
@Composable
functions always use
XYImmutable
classes as parameters. State modification happens only inside
mutateState {...}
calls, so the mutable variant of the domain model is used only in the lambda of
mutateState {...}
calls. After modifying the state, Compose will recompose the UI based on the current immutable state, so Compose works only with immutable data. To summarize, this PoC library is not a big magic, it only makes the application's state model more readable and free of boilerplate code, by generating immutable variants of the model for efficient recompositions, and mutable variants for efficient state modification.
t

Tash

06/07/2022, 12:24 AM
OH! so it uses a custom
MutableObjectState
, I was getting confused, thinking that it used Compose’s Snapshot
MutableState<T>
😅
n

Norbi

06/07/2022, 5:52 AM
@Tash, yeah, sometimes it is really difficult to find good names for classes... 😉 😄
z

Zach Klippenstein (he/him) [MOD]

06/07/2022, 3:41 PM
Why do you need to
MutableObjectState
class?
TodoItemMutableImpl
could just directly use MutableStates for its properties.
🤔 2
7 Views