Problem of mutating data class for use in Jetpack Compose

The problem:
The difficulty of maintaining the “stability” of large classes for Compose and mutability. The inevitability of the boilerplate code.

I ran into the problem that Kotlin together with Jetpack Compose cannot handle working with large data models. Real cases often requires the declaration of many large classes in which there are several dozen fields.

Example:

Let’s imagine that we have a report document that contains many tables, lists, calculations and about 400 fields in total. (this is from a real case).

For Jetpack Compose, you need to make the data model “stable” (without var values and mutable collections) in order to avoid unnecessary recompositions. But at the same time leave the option to change the data in the class.

Currently, there are several approaches, but they all have significant disadvantages:

// Here is our lightweight data model

data class Document(
    val form1: Form1
)

data class Form1(
    val list: List<Item>
)

data class Item(
    val property1: Int,
    val property2: List<NumberField>,
)

data class NumberField(
    val value: Double = 0.0,
    val errorMsg: String? = null,
    val path: String
)

The data class approach

+ Can be serialized from/to JSON

+ Can be used in the UI and DATA modules without creating mappers without violating the practice of clean code.

+ Convenient to refactor

- An absolutely inconvenient API for mutating data which is a significant disadvantage.

fun dataClassCopy() {
    val document = Document(...)

    document.copy(
        form1 = document.form1.copy(
            list = document.form1.list.toMutableList().apply {
                val newItem = this[0].copy(
                    property2 = this[0].property2.toMutableList().apply {
                        val newField = this[0].copy(value = 2.0)
                        set(0, newField)
                    }.toList()
                )
                set(0, newItem)
            }.toList()
        )
    )
}

The mutableState approach from the Compose Runtime library

+ Convenient data mutation

- Cannot be used in the data layer as you will have to connect the compose runtime to it which is extremely bad.

- Can’t serialize from/to JSON out of the box

- You will have to create a huge mapper in order to convert the model into a suitable one for the data layer and back

- Considering the mappers, refactoring will take forever.

fun compose() {
    val document = UiDocument(...)
    // using SnapshotStateList and MutableState<T> from Compose Runtime
    document.form1.list[0].property2[0].value = 2.0
}

The arrow-kt library approach

+ Reduces the amount of code required for class mutation

- It is still not effective for large data models.

- The solution is not out of the box and has a confusing API.

fun arrowKt() {
    val document = Document(...)

    val updatedDocument = (Document.form1 compose Form1.list)
        .index(Index.list(), 0)
        .compose(Item.property2)
        .index(Index.list(), 0)
        .compose(NumberField.value)
        .set(document, 2.0)
}

None of the above methods of solving the problem is optimal for large projects with huge data models.

Suggested solution

I understand that it’s unlikely that mutable variables will ever be added to Compose, so
It would be good to have some kind of mechanism that could mutate data classes normally.
Or a keyword/annotation for the type of data that would be appropriate to add to both ui and data modules.
For example something like this:

fun suggestedSolution() {
    val document = Document(...)

    val immutableCopy = document.mutate {
        this.form1.list[0].property2[0].value = 2.0
    }
}

Kotlin could natively create a temporary mutable version of a class based on the original data class and return an immutable modified version.

1 Like

You can check GitHub - PatilShreyas/mutekt: Simplify mutating "immutable" state models (a Kotlin multiplatform library) . I never used it myself and I don’t know if it has support for turning read-only collections into mutable ones, but still, it should safe a lot of pain for you.

Don’t overtrust AI. Its Arrow example is not how you’d actually do it. I don’t have a project in front of me right now, but I’m pretty sure it’d look more like:

fun arrowKt() {
    val document = Document(...)

    val updatedDocument = document.copy {
        form1.list.index(0).property2.index(0).value set 2.0
    }
}

which is very very close to your example. In fact, you could probably add a operator fun get that does index so that the code is even closer. The only different then would be the set, which I’m wanting to fix eventually (with a compiler plugin or context parameters, but the latter doesn’t support contexts on delegated properties yet)

2 Likes

Hi, thanks for the reply. I’ve already tried this solution, it doesn’t know how to work with nested classes, and it turns out that it’s the same copy, only with extra steps.

Hi, thanks for the reply. arrow-kt may be the solution. I actually used AI to generate an example of arrow-kt code, because when I went to the ArrowKt documentation, I was freaked out by their examples. I’m like, how could this be better than a copy of a data class. Your example clarified the situation.