3

Let's imagine we have the next 5 lines of code:

@Composable
fun MyParentComposable() {
    var myVariable by remember { mutableStateOf(0) } 
    MyChildComposable(myVariable)
}
  • I know whenever we change the myVariable we're going to recompose MyParentComposable and MyChildComposable and any other children inside the MyChildComposable() reading the value.

  • I know that if I don't use the myVariable in any composable inside MyChildComposable it will be still be recomposed when changing it because we're reading it somewhere (I'd guess in the parameter even though it's unused)

  • I know that if we pass a lambda and defer the read, then only the component that invokes the value and the parent scope will be recomposed MyChildComposable.

The question is, when passing myVariable to MyChildComposable, am I reading it or is there something else?

I wanted to see some decompiled code or something like that to understand it a bit deeper but I don't know where should I go. Hopefully someone can throw some light here to have something that I can say 'yeah it's because of that'

Same goes for this example

@Composable
fun MyParentComposable() {
    val myVariable = remember { mutableStateOf(0) } 
    MyChildComposable(myVariable.value)
}

I'm reading the value in the MyParentComposable() scope and passing it down MyChildComposable()

Edit:

Example without Lambda: ParentComposable and child get recomposed without any composable inside reading it, only a composable with a parameter

@Composable
fun MyParentComposable() {
    var myVariable by remember { mutableStateOf(0) }
    MyChildComposable(myVariable)

    Button(onClick = { myVariable += 1}) {
        Text(text = "Click")
    }
}

@Composable
fun MyChildComposable(myVariable: Int) {
// Without text, only the parameter there
}

Example With Lambda: Only ChildComposable gets recomposed after reading it inside.

@Composable
fun MyParentComposable() {
    var myVariable by remember { mutableStateOf(0) }
    var myLambdaVariable = { myVariable }
    MyChildComposable(myLambdaVariable)

    Button(onClick = { myVariable += 1}) {
        Text(text = "Click")
    }
}

@Composable
fun MyChildComposable(myLambdaVariable: () -> Int) {
    Text(text = "${myLambdaVariable()}")
}

Now the question is, why does the example WITHOUT lambda recomposes the child: Is it because passing the parameter is considered as reading? Is it because you're already reading it before passing by just the fact of doing: MyChildComposable(anyVariableHere) <-- Considered as reading in ParentComposableScope

I know using the by will cause a trigger read. But I need to understand what's triggering the recomposition, if reading it in ParentComposable plus setting it in the parameter of the ChildComposable. Does compose detects automatically this function is reading a property, is it considered reading as having settled in the parameter.

I want fine-grained information to understand what's happening when we set a parameter a parameter to the ChildComposable and even though "we're not reading it" the ChildComposable gets recomposed

Barrufet
  • 495
  • 1
  • 11
  • 33
  • 1
    Very good question, and upvoted it. I answered several questions about scoped recomposition or state-deferring with lambdas and preventing composables between when lambda is passed but never seen a source that explains why Composables between that don't directly read a value are recomposed. This is about state deferring https://stackoverflow.com/questions/73680749/android-jetpack-compose-composable-function-get-recompose-each-time-text-field/73681007#73681007. I think it's related to parameters changing in the slot table causing recomposition – Thracian Sep 23 '22 at 05:41
  • 1
    Maybe `Storing prameters` section of this article from Leland Richardson explains it but not 100% sure. https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd – Thracian Sep 23 '22 at 05:41
  • 1
    When you call the `MyChildComposable()` constructor, and pass it **whatever**, that __whatever__ will be read, and placed on the stack, before the function call. Usually, the caller is the one to place arguments on the stack. This is typically a language-agnostic thing present in computer science, so it's not just C/Java or compiler dependant. Even in C, while I'm not sure on this - i think it's standardized on who actually stores/restores the registers before/after the call, however I know that in some compilers the caller stores/restores, while in other the callee stores/restores. – Shark Sep 23 '22 at 14:15
  • Thanks everyone, your links resources and explanations helped me, and also @Shark 's answers made sense together with the Uli's answer! – Barrufet Sep 23 '22 at 18:35

2 Answers2

5

Your question touches on 2 separate concepts, and has a succinct answer:

  1. Passing a parameter implies first reading it (in the caller). This is a basic concept of imperative programming and is modeled no differently in the case of Compose. The caller reads a (state) variable value from a memory location to put it on the stack, from which the called function retrieves it. Because the caller has to read the variable to pass its value, a change in the value must trigger caller recomposition.
  2. Skipping if the inputs haven't changed: when the input (parameter) of the child composable changes it will recompose. This is independent of whether the child composable actually uses the parameter value: https://developer.android.com/jetpack/compose/lifecycle#skipping

This is the basic thought model to use in this case to explain recomposition behavior; there may be optimizations under the hood, but it does not change the modeled behavior you see.

Keep in mind that the rules of recomposition are not set in stone, and may change; ideally you would not rely on recomposition necessarily acting this way, the model is useful mainly for optimizations that are valid at the present time.

Uli
  • 2,828
  • 20
  • 23
  • actually much better said than i did. +1 – Shark Sep 23 '22 at 14:15
  • First of all, thank you so much for this detailed answer, I think I understood it and it makes absolute sense, but I have a question: "Passing a parameter implies first reading it (in the caller)" - So in order for the ChildComposable() to get the passed variable first must be read in the ParentComposable (The caller) and then passed to the ChildComposable. – Barrufet Sep 23 '22 at 18:29
  • Also I'd like to know where's the right place to get the information for the answer 1. Did you go to Kotlin docs, any book talking about imperative paradigm or anything? I feel like I could step up my coding sense if I had this information in mind, or at least know how these piece together, thank you! :D – Barrufet Sep 23 '22 at 18:38
  • 1
    @Barrufet The way you paraphrased it sounds good to me. As for a reference for number 1: you should be able to find information on parameter passing to functions by Googling it; perhaps search for something like "passing function call parameters on stack"; loading variable values into a register for further use is a "von Neumann machine" concept, I think you'd find it via that term. It would be nice if Compose documentation made all this more explicit, it's a good and valid question. – Uli Sep 24 '22 at 01:15
  • 1
    Also, I should make it clear that my answer is written from the angle of a mental model, which is the angle that the official Compose documentation takes, and which I think is the most useful for daily usage. What I've called "optimizations under the hood" may in reality look quite different, like the link from Thracian to Leland's article might show you. It just tends to be easier to use a mental model describing what happens rather than think about slot table implementations. Still, it might be worth it for you to dig for authoritative details in Leland's post. – Uli Sep 24 '22 at 01:47
1

It's important to note that reading is triggered because the getter of the local property is called. This is due the fact you used by to delegate the keyword and any usage (even passing as parameter) will be considered as call to the getter.

If you used assignment, passing it as paramere will not trigger the getter, but call to the .value

Nikola Despotoski
  • 49,966
  • 15
  • 119
  • 148
  • So, let's say I have `val myVariable = remember { mutableStateOf(0) }` and then I go to `MyChildComposable(myVariable.value)`, in this specific case it wil have the same effect as the previous one right? You're still reading the value in the `ParentComposable()` scope, both will be recomposed – Barrufet Sep 22 '22 at 03:54
  • Yes. `.value` will trigger the recomposition. – Nikola Despotoski Sep 22 '22 at 10:09
  • I appreciate your effort but I think I still haven't understood what's happening. Please, feel free to see my edit, I try to understand if setting this variable there is considered as reading, because the way I see it it's like: "I'M GIVING AN OPPORTUNITY for the variable to be in this composable, but in the end I'm not doing anything with that variable." but doesn't seem to be the case. Is there any docs that says what goes under the hood when you set this parameter and receive a value? Or anything to understand why it gets recomposed? – Barrufet Sep 23 '22 at 04:14