I've been running some experiments based on <the e...
# compose
m
I've been running some experiments based on the excellent post by @Zach Klippenstein (he/him) [MOD] regarding scope of recomposition. I feel like I'm getting different results, and I'm not sure where I'm going wrong in my analysis. Details in 🧵 .
👀 3
I have this in a scrap project:
Copy code
@Composable
private fun RecomposeTest() {
  val switchState = remember { mutableStateOf(false) }

  Log.d("wut?", "RecomposeTest()")

  Row(modifier = Modifier.padding(8.dp)) {
    Switch(
      checked = switchState.value,
      onCheckedChange = { switchState.value = it },
      modifier = Modifier.padding(end = 4.dp)
    )
    Text(stringResource(R.string.app_name))
  }
}
I had expected that only the
Switch()
would get recomposed when the
Switch()
itself is clicked, as the call to
Switch()
is the only place where I seem to reference that
switchState
value. Yet, I get the log message on every click of the
Switch()
. Unless I am misunderstanding something, that means that
RecomposeTest()
is being recomposed, not just the
Switch()
, or even the
Row()
. FWIW, I see this with
beta01
and
beta04
.
The
RecomposeTest()
is being called from the
setContent()
call in the activity:
Copy code
class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MyApplicationTheme {
        // A surface container using the 'background' color from the theme
        Surface(color = MaterialTheme.colors.background) {
          RecomposeTest()
        }
      }
    }
  }
}
Otherwise, the project is standard Arctic Fox Canary 12 "Empty Compose Activity", except for bumping things up to
beta04
after realizing that the template was at
beta01
.
So, the tactical question is: why is that
Log.d()
call being made for every
Switch()
click? The more interesting question is: how do folks like me figure out what is going on here? IOW, is there a way that I can determine why the Compose runtime is deciding that
RecomposeTest()
needs to be called again?
s
Short answer:
Row
is inline
☝️ 1
Longer answer, parameters are resolved to values in the parent recompose scope, so any parameter that reads a state means the calling site is what recomposes, not the callee
In this case you're passing:
Copy code
checked = switchState.value
and the read happens there (when resolving the function parameters)
m
right, and with
Row()
being
inline
, that's actually in the body of
RecomposeTest()
🤦🏻‍♂️
s
yep
m
OK, that's going to take some getting used to, but it makes sense. Now I just gotta figure out how to explain that sort of behavior in an introductory Compose book, without breaking anyone's 🧠 ... Thanks for the clarification!
s
hah
m
I suspect strongly that I will wind up with the The Sean McQuillan Approach™️ of "don't worry, Compose will figure out the right scope" 😁
s
HAHA
I was just writing that 🙂
But really, I'd advise to be cautious about what you show in a book as this runtime behavior is not guaranteed to be stable at all (and future optimizations are likely)
👍 2
If you do want to show an example, I'd suggest doing something you have full control over and do the state read clearly in the composition scope such that it could never realistically be optimized
e.g.
Copy code
val aColorForSomeReason = if (switchedState.value) { Color.Red } else { Color.Blue }
m
Understood. At the same time, Compose's compiler magic makes logging more... interesting, in the "may you live in interesting times" sort of interesting. For example, logging a state value affects the scope of recomposition, if your logging is outside what might ordinarily have been recomposed. At some point, I want to warn developers about these sorts of impacts.
s
Yea
I think it's a good chapter actually
I'd just avoid (1) any builtins [they're very likely to change behavior] (2) anything that could be optimized due to only being used once with no side effect from the value
m
how are you defining "builtins"? do you mean the standard composables like
Row()
?
s
yea
🆗 1
Defining a regular Composable fun is gonna get you a restart scope 🙂
And the composable lambda behavior is probably the big trick to show off
m
OK, thanks again!
z
Late to this thread but I do call out the inline caveat at the end of the post. Perhaps it should be more prominent? It’s easy to miss, and since some of the most common composable wrappers are inline maybe it’s worth bringing it forward a bit
j
Here’s the link to a relatively old convo where I was asking a similar question to which @Adam Powell and @Chuck Jazdzewski [G] answered with very knowledgeable insights. https://kotlinlang.slack.com/archives/CJLTWPH7S/p1611232921023400
m
I do call out the inline caveat at the end of the post
I saw that, both before and after I started this thread. In my case, it was mostly a matter of "not putting two and two together" with what you had there and the symptoms that I was seeing. IOW, 🤦🏻‍♂️ . Again, the post was great!
🙏 1
z
I pulled the paragraph about inline functions into its own heading and added some more detail and an example to hopefully make it easier to catch.
🙏 2