m

    Mark Murphy

    1 year ago
    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 🧵 .
    I have this in a scrap project:
    @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:
    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?
    Sean McQuillan [G]

    Sean McQuillan [G]

    1 year ago
    Short answer:
    Row
    is inline
    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:
    checked = switchState.value
    and the read happens there (when resolving the function parameters)
    m

    Mark Murphy

    1 year ago
    right, and with
    Row()
    being
    inline
    , that's actually in the body of
    RecomposeTest()
    🤦‍♂️🏻
    Sean McQuillan [G]

    Sean McQuillan [G]

    1 year ago
    yep
    m

    Mark Murphy

    1 year ago
    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!
    Sean McQuillan [G]

    Sean McQuillan [G]

    1 year ago
    hah
    m

    Mark Murphy

    1 year ago
    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" 😁
    Sean McQuillan [G]

    Sean McQuillan [G]

    1 year ago
    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)
    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.
    val aColorForSomeReason = if (switchedState.value) { Color.Red } else { Color.Blue }
    m

    Mark Murphy

    1 year ago
    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.
    Sean McQuillan [G]

    Sean McQuillan [G]

    1 year ago
    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

    Mark Murphy

    1 year ago
    how are you defining "builtins"? do you mean the standard composables like
    Row()
    ?
    Sean McQuillan [G]

    Sean McQuillan [G]

    1 year ago
    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

    Mark Murphy

    1 year ago
    OK, thanks again!
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    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

    julioromano

    1 year ago
    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

    Mark Murphy

    1 year ago
    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!
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    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.