Pass by Value vs CompositionLocal vs Static CompositionLocal

Examples to show how to pass data to composable functions using function parameters(i.e. pass by value), CompositionLocal and static CompositionLocal

ยท

5 min read

Pass by Value vs CompositionLocal vs Static CompositionLocal

There are a few ways you can pass data to a composable function:

  • Pass by Value (function parameter)

  • CompositionLocal

  • Static CompositionLocal

Pass by Value is a conventional way. CompositionLocal and static CompositionLocal is a Jetpack Compose way, but static CompositionLocal is useless in my opinion (will be explained later).

Pass by Value

This is a very simple example to pass counter value to the Parent() composable function, then increments it by 1 and passes it to the Child() composable function. Finally, it calls the GrandChild() composable function without any parameters.

Let's investigate the code, what do you think the Logcat outputs are during the first composition and the subsequent recomposition?

private val tag = "CompLocal"

@Composable
fun PassByValueDemo() {

    var counter by remember {
        mutableStateOf(-1)
    }

    MyButton(onClick = { ++counter }, text = "PassByValue Demo")

    if(counter < 0) return

    Log.d(tag, "************** Pass by Value **************")
    Parent(counter)
}

@Composable
private fun Parent(value: Int) {
    Log.d(tag, "Start Parent - value: $value")
    Child(value + 1)
    Log.d(tag, "End Parent - value: $value")
}

@Composable
private fun Child(value: Int) {
    Log.d(tag, "Start Child - value: $value")
    GrandChild()
    Log.d(tag, "End Child - value: $value")
}


@Composable
private fun GrandChild() {
    Log.d(tag, "Start GrandChild")
    Log.d(tag, "End GrandChild")
}

Logcat Output - First Composition (first clicked)

value is incremented by 1 and passed into the Child() composable

************** Pass by Value **************
Start Parent - value: 0
Start Child - value: 1
Start GrandChild
End GrandChild
End Child - value: 1
End Parent - value: 0

Logcat Output - Recomposition (second clicked)

A very important thing to notice is the GrandChild() composable is skipped.

************** Pass by Value **************
Start Parent - value: 1
Start Child - value: 2
End Child - value: 2
End Parent - value: 1

CompositionLocal

To accomplish the exact behavior, CompositionLocal can be used.

Here are the simple steps:

  1. Create a CompositionLocal variable (using compositionLocalOf() that is accessible from the compostable functions that you want to use it.

     private val LocalInt = compositionLocalOf { 0 }
    
  2. Provide value to the CompositionLocal (i.e. LocalInt) using CompositionLocalProvider.

     CompositionLocalProvider(
         LocalInt provides 0,
     ) {
         //call your composable function here
     }
    
  3. Access the CompositionLocal's value by CompositionLocal.current.

     LocalInt.current
    

The full code looks like this

private val LocalInt = compositionLocalOf { 0 }
private val tag = "CompLocal"

@Composable
fun CompositionLocalDemo() {

    var counter by remember {
        mutableStateOf(-1)
    }

    MyButton(onClick = { ++counter }, text = "CompositionLocal Demo")

    if(counter < 0) return

    Log.d(tag, "************** Using CompositionLocal **************")
    CompositionLocalProvider(
        LocalInt provides counter,
    ) {
        Parent()
    }
}

@Composable
private fun Parent() {
    Log.d(tag, "Start Parent - LocalInt: ${LocalInt.current} ")

    CompositionLocalProvider(
        LocalInt provides LocalInt.current + 1,
    ) {
        Child()
    }

    Log.d(tag, "End Parent - LocalInt: ${LocalInt.current}")
}

@Composable
private fun Child() {
    Log.d(tag, "Start Child - LocalInt: ${LocalInt.current} ")

    GrandChild()

    Log.d(tag, "Emd Child - LocalInt: ${LocalInt.current} ")
}

@Composable
private fun GrandChild() {
    Log.d(tag, "Start GrandChild")

    Log.d(tag, "End GrandChild")
}

This has the same outputs as the Pass by Value example above.

Logcat Output - First Composition (first clicked)

************** Pass by Value **************
Start Parent - value: 0
Start Child - value: 1
Start GrandChild
End GrandChild
End Child - value: 1
End Parent - value: 0

Logcat Output - Recomposition (second clicked)

************** Pass by Value **************
Start Parent - value: 1
Start Child - value: 2
End Child - value: 2
End Parent - value: 1

Static CompositionLocal

You can replace CompositionLocal with static CompositionLocal. This code

private val LocalInt = compositionLocalOf { 0 }

is replaced by

private val LocalInt = staticCompositionLocalOf { 0 }

and everything remains the same.

However, the outputs are NOT the same as Pass by Value and CompositionLocal. Changes to the CompositionLocal's value triggers the entire composition tree to be recomposed.

Logcat Output - First Composition (first clicked)

************** Pass by Value **************
Start Parent - value: 0
Start Child - value: 1
Start GrandChild
End GrandChild
End Child - value: 1
End Parent - value: 0

Logcat Output - Recomposition (second clicked)

************** Pass by Value **************
Start Parent - value: 1
Start Child - value: 2
Start GrandChild
End GrandChild
End Child - value: 2
End Parent - value: 1

As you can see, GrandChild() composable function is called/recomposed even though it doesn't access the LocalInt.current value. This is a complete waste of unnecessary recompositions in my opinion.

The official document states that you should only use staticCompositionLocalOf() for a value that doesn't change. But the issue is, how do you prevent the user or any developer from changing it? You can't.

Therefore, it seems to be we should just use CompositionLocalOf() and NOT use staticCompositionLocalOf() as a best practice.

The official document does mention about performance benefits of using staticCompositionLocalOf if the value is not changed, but how much benefits exactly?

I agree to use staticCompositionLocalOf() only if it is a constant value and can't be changed. Then, this prevents the users from misusing it. What do you think?

Conclusion

CompositionLocal is just a Jetpack Compose way as a replacement of passing by value to a composable function. This may be helpful if you have a global variable that is often being used by your composable functions.

Static CompsitionLocal triggers the entire composable tree to be recomposed if its value is changed. So, use it carefully. My recommendation is, don't use it.

Source Code

GitHub Repository: Demo_UnderstandComposeConcept

Did you find this article valuable?

Support Vincent Tsen by becoming a sponsor. Any amount is appreciated!

ย