Skip to main content

Command Palette

Search for a command to run...

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

Updated
5 min read
Pass by Value vs CompositionLocal vs Static CompositionLocal
V

I'm a self-taught hobbyist Android developer who loves to build projects and share valuable tips for new Android developers.

Feel free to comment, share or connect with me!

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

A
Alek3y ago

Hay Vincent, i have problem with compose, i have small app like cart, but i need the price is editable in textfield, i try to resolve but still failed, can you give me some insight to do it thanks

1
V

I'm not sure if I can help, but something like this will do.

@Composable
fun SimpleFilledTextFieldSample() {
    var text by remember { mutableStateOf("Hello") }

    TextField(
        value = text,
        onValueChange = { text = it },
        label = { Text("Label") }
    )
}

Android App Dev

Part 10 of 47

Discover helpful tips and tricks for Android app development and Jetpack Compose through my personal journey and experience.

Up next

Android Context 101 with Class Diagram

Beginner's Guide to understand the fundamental of Context with class diagram in Android app development