Dilraj Singh
07/05/2023, 5:55 AMdata class ExampleModel(
val field1: Int = 0,
val field2: Int = 0,
val field3: Int = 0,
...
)
Typically we would model this as follows-
1. MVVM- we would have individual liveData/stateFlow
for each state element, and whenever we want to update any state element, we would update the stateFlow for that particular element
2. MVI- we would encapsulate all the elements into a single immutable data class
, and whenever we want to update any state element, we would make a copy of this data class object, and mutate the required property
The first question is whether MVI will cause a lot of memory overhead since we make a copy of the object every time we want to update any element of the state. In a timer application where the state can be modelled as1. MVI-, and update the state as follows-data class TimeModel(val remainingTime: String)
stateFlow.update { timeModel.copy(remainingTime = timeModel.remainingTime - 1) }
2. MVVM- stateFlow.update { it - 1 }
In both cases, we are making a new string object, but in MVI we are making a new object for TimeModel
as well. And since kotlin introduced value classes to reduce the memory footprint of simple model classes, is MVI nullifying that advantage?
The second question is that while using jetpack compose, does it make more sense to use MVVM for bigger projects than MVI? In MVI, we would make a copy of the class object, thus changing its. So in every composable, we would need to have ahashCode
just to reduce the number of recompositions caused by other state updates, otherwise, it would trigger a recomposition even if some other state element is changing. Example below-derivedStateOf
@Composable
fun ExampleComposable1(field1: () -> Int) {
println("ExampleComposable1: recomposing")
Text(text = "field 1 is ${field1()}")
}
@Composable
fun ExampleComposable2(field2: () -> Int) {
println("ExampleComposable2: recomposing")
Text(text = "field 2 is ${field2()}")
}
@Composable
fun ExampleComposable3(field3: () -> Int) {
println("ExampleComposable3: recomposing")
Text(text = "field 3 is ${field3()}")
}
data class ExampleModel(
val field1: Int = 0,
val field2: Int = 0,
val field3: Int = 0,
)
var state by remember {
mutableStateOf(ExampleModel())
}
Button(
onClick = { state = state.copy(field1 = state.field1 + 1) }
) {
Text(text = "update state 1")
}
ExampleComposable1(field1 = { state.field1 })
ExampleComposable2(field2 = { state.field2 })
ExampleComposable3(field3 = { state.field3 })
If we check the logs, we can see that all the 3 composables recompose every time even though we are updating only field1
, and the other 2 are constant. To solve this problem, we would have to use derivedStateOf
inside every composable to check if the value is changing or not, thus creating even more state objects, and increasing memory footprint. I understand this would not hamper an example application, but in a production application, where new features are added every day, this will become a huge problem.curioustechizen
07/05/2023, 1:46 PMdispatch()
) that accepts an interface/sealed interface/sealed class.
So in my understanding of these patterns, your first question is not valid. Neither MVVM nor MVI dictate the granularity of your state. Both patterns allow you to have stateFlows for individual elements or to have a single immutable data class the combines all state elements.curioustechizen
07/05/2023, 1:54 PMExampleModel
as an input, but the closer you go to the leaves of your tree, the more likely that the composables accept more primitive types as inputs; and the more likely that those inputs did not change.
I guess another way of putting this is - yes maybe composables closer to the top of the tree do recompose a lot but it is likely that those further near the leaves will skip recomposition.
But also, remember that recomposition in itself is not evil. If your composables are not doing much then it is likely that recomposition is not even noticeable.Dilraj Singh
07/05/2023, 2:25 PMcurioustechizen
07/05/2023, 4:05 PMcurioustechizen
07/05/2023, 4:14 PMFooScreen
) will still accept the FooState
object.
But FooScreen
will be made up of several sections. Not every section requires all the pieces from FooState
.
So FooScreen
will call Section1
, Section2
etc.
I'm suggesting that these smaller composables that make up the sections of your screen do not necessarily need to take FooState as an input. They could take a small data class that is only relevant for them, or they could just operate on primitives.
The implication is that maybe FooScreen
recomposes very often but when Section1
recomposes, maybe Section2
is skipped, and so on.curioustechizen
07/05/2023, 4:15 PMManish Malviya
07/10/2023, 6:04 PM@Stable
in your case as you are passing primitive types they are by default stable and instead of passing data through lambda, passing Int directly will skip recompositions.Manish Malviya
07/10/2023, 6:07 PMMutableSharedFlow
and can set replay cache to 0 hence your previous models won't be rememberedDilraj Singh
07/19/2023, 8:14 AMderivedStateOf
construct, as it won't be treated as a state readDilraj Singh
07/19/2023, 8:22 AMdata class Counter(val intValue: Int)
var counter by remember {
mutableStateOf(Counter(1))
}
// update counter state
StateParameter(counter = counter.intValue)
@Composable
fun StateParameter(counter: Int) {
val derivedCounter = remember {
derivedStateOf { counter % 5 == 0 }
}
Text(text = "count is ${derivedCounter.value}")
}
curioustechizen
07/19/2023, 9:24 AMcurioustechizen
07/19/2023, 9:25 AMIf we pass primitives to our composable, then we won't we able to use derivedStateOf construct, as it won't be treated as a state read
Are you sure about that? As far as I know there's no difference in the way
derivedStateOf
treats primitives versus objects.curioustechizen
07/19/2023, 9:29 AMDilraj Singh
07/19/2023, 10:23 AMcounter = counter.copy(intValue = counter.intValue + 1)
my end goal is to have a single pattern that all the composables should follow with MVI, and the points of comparison being-
1. should we pass state inside a lambda so that we can use derived state, with the caveat that it will recompose everytime the parent state changes
2. should we pass state as a function parameter so that compose should skip unchanged parameters, with the caveat being that derived state cannot be used anywhere
or we could have a pattern where both things work