Android Compose side effects: Side-Effect behavior

I'm probably missing some important composable recomposition concepts here but let me explain my doubt.

I was reading about side effects in Android Compose and I findout that specifically SideEffect composable function gets executed only when the parent composable function is recomposed. Meaning, every time parent composable function is recomposed, any effect of a SideEffect will be executed. Now, recomposition happens when:

  1. A composable function is reinvoked with different parameters.
  2. Internal state in the composable function changes.

So if we have this simple counter composable function called Counter1, SideEffect inside of it will be executed when this function first launches and, after that, every time the user increases the count (every time the user clicks on the Button):

@Composable
fun Counter1() {
    var counter: Int by remember { mutableStateOf(0) }

    SideEffect {
        Log.d("Test tag", "Counter1: $counter")
    }

    Column {
        Button(onClick = { counter++ }) {
            Text(text = "Increase count")
        }
        Text(text = "Counter value is: $counter")
    }
}

If we change that counter a little bit and we produce this other one:

@Composable
fun Counter2() {
    var counter: Int by remember { mutableStateOf(0) }

    SideEffect {
        Log.d("Test tag", "Counter2: $counter")
    }

    Column {
        Button(onClick = { counter++ }) {
            Text(text = "Increase count is: $counter")
        }
    }
}

SideEffect in this second scneario will only be executed the first time the composable is recomposed. It will not be executed when the user changes the state (by increasing the counter). Why is this happening? I put a break point at the first line in Counter2 and every time the user clicks on the button, the counter value is increased and the Counter2 is reinvoked with a new state.

  1. Why is side effect not being executed in Counter2 after the state changes and Counter2 is recomposed?
  2. Is only Text function inside Button function being recomposed?
  3. If 2. is true, why is not the entire Counter2 being recomposed but Counter1 is?

Thanks in advance team!

Upvotes: 3

Views: 678

Answers (1)

tyg
tyg

Reputation: 14984

To expand on @Thracian's comment, what SideEffect interprets as a recomposition is restricted to the part of the compose tree where SideEffect is located. Yes, there is a recomposition everytime the counter is increased, but not everything is recomposed, just the affected composables.

Although the counter state variable in Counter2 is in the same compose scope as the side effect, it is not used anywhere in that scope so no recomposition is necessary (and the side effect is skipped). It is used in the Button composable to call Text, but that is a separate compose function that has its own scope.

The interesting part is why Counter1 behaves differently. Now the counter is used only in the Column composable to call Text, but Column also is another composable function that should have its own scope and should therefore behave the same (which it doesn't).

Here is where @Thracian's comment comes into play: Column is not just a compose function, it is also declared as an inline function. That means that the compiler inlines the function by replacing every call of the function with a copy of its body, then the function is removed. In the compiled code there is no call to another compose function, therefore no new compose scope is created and the content of Column uses the surrounding scope instead. And that happens to be the same where the SideEffect is located.

That is why changing counter in Counter1 triggers a recomposition of the scope that contains the SideEffect, because it is the same that contains the Text (that accesses counter).

Upvotes: 2

Related Questions