That’s quite disappointing, is there anything I ca...
# compose
g
That’s quite disappointing, is there anything I can do to make Compose aware of the inside of an object ?
m
You could make
CalculatorState
be a
data class
with
val
properties, then use
copy()
to create the mutated object that you use to update
calculatorState
Or, you could have the individual properties of
CalculatorState
be
mutableStateOf()
property delegates. Those correspond to alternatives 1 and 2 for the replacement of
@Model
as noted in this Gerrit entry: https://android-review.googlesource.com/c/platform/frameworks/support/+/1311293/
👍 4
☝️ 1
a
Yep, this is the way to do it
I would strongly suggest going all the way for whichever approach you pick though: if you choose the immutable state object held in a
MutableState<CalculatorState>
, great, but if you make
CalculatorState
mutable by way of
mutableStateOf
property delegation, don't use
State<CalculatorState>
to hold it
for all of the same reasons you would do
var list: List<T>
or
val list: MutableList<T>
but not
var list: MutableList<T>
have either the object or the reference to that object be mutable, but not both.
g
Thank you both, really Interesting! If the object is mine and I call it SomethingState, I would expect Compose to be aware of it so I will use
mutableStateOf()
for its properties. Using
copy
feels less clean memory-wise, but a good option if the object is not mine (ex: 3rd library)
a
Sounds like a good plan, but the memory story isn't necessarily cut and dry.
mutableStateOf
creates a mutable box, and it does so with generics so anything you put in it is boxed as well. (We may do some additional optimizations around that last part later.) We also track historical values of
MutableState
objects to some extent in the form of the snapshot records. Consider the API ergonomics you're trying to create here first and please report back if you experience issues and we'll see what we can do to optimize.
You're also very close to a pattern we like a lot for mutable hoisted state objects here if you go with that approach:
Copy code
@Composable
fun CalculatorScreen(
    calculatorState: CalculatorState = remember { CalculatorState() }
) {
    // ...
this lets your
CalculatorScreen
be self-sufficient in the simple case, but a caller that wants to also manipulate its
CalculatorState
via its public API can do so. This is also great for testability, since it means that
CalculatorScreen
itself is stateless and you can pass it any configuration you like to control and test its behavior.
it also means that by definition your
CalculatorState
object becomes a lot more testable, so you can decouple testing of your logic (in
CalculatorState
) from testing the UI presentation itself
g
That’s just amazing, I will use
remember
! I haven’t gotten to testing yet, but it seems a thousand years easier than with the old UI framework.
I see in JetNews, that’s the pattern used indeed. I didn’t understand the reason for all those
mutableStateOf
but now it makes sense 🙂
Hey @Adam Powell I tried to use
CalculatorState
in a junit simple test (not AndroidTest) and could not:
Copy code
java.lang.IllegalStateException: Not in a frame

	at androidx.compose.frames.FramesKt.currentFrame(Frames.kt:180)
	at androidx.compose.FramedMutableState.setValue(MutableState.kt:296)
	at il.co.galex.alexpizzapp.feature.calculator.CalculatorState.setNumberOfBalls(CalculatorState.kt:14)
	at il.co.galex.alexpizzapp.feature.calculator.CalculatorTest.fourPizzasOf280(CalculatorTest.kt:22)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
So I am bound to test this code as an AndroidTest? In this case a simpler object using
copy
would be easier to test, right?
a
This might be a, "wait until Wednesday's dev15 release" for the easiest fix in the form of the newer Snapshot replacement for Frames
You can poke around a bit in the source tree for the parts to initialize frames outside of the usual Android setup harness if you want something sooner
g
Ahaha I can wait till Wednesday 😄 Thanks!