0

when I use CompositionLocal, I have got the data from the parent and modify it, but I found it would not trigger the child recomposition.

I have successfully change the data, which can be proved through that when I add an extra state in the child composable then change it to trigger recomposition I can get the new data.

Is anybody can give me help?

Append

code like below


data class GlobalState(var count: Int = 0)

val LocalAppState = compositionLocalOf { GlobalState() }

@Composable
fun App() {
    CompositionLocalProvider(LocalAppState provides GlobalState()) {
        CountPage(globalState = LocalAppState.current)
    }
}

@Composable
fun CountPage(globalState: GlobalState) {
    // use it request recomposition worked
//    val recomposeScope = currentRecomposeScope
    BoxWithConstraints(
        contentAlignment = Alignment.Center,
        modifier = Modifier
            .fillMaxSize()
            .clickable {
                globalState.count++
//                recomposeScope.invalidate()

            }) {
        Text("count ${globalState.count}")
    }
}

I found a workaround is using currentRecomposable to force recomposition, maybe there is a better way and pls tell me.

z.g.y
  • 5,512
  • 4
  • 10
  • 36
wjploop
  • 209
  • 2
  • 8
  • 1
    @wjploop I totally understand your question. I also come from React, and in there you can do this with the Context API. I still haven't figured out how to do this with Compose. If you have, please share it! Thanks! – Silas Pedrosa Nov 25 '21 at 11:53

3 Answers3

5

The composition local is a red herring here. Since GlobalScope is not observable composition is not notified that it changed. The easiest change is to modify the definition of GlobalState to,

class GlobalState(count: Int) {
   var count by mutableStateOf(count)
}

This will automatically notify compose that the value of count has changed.

chuckj
  • 27,773
  • 7
  • 53
  • 49
4

I am not sure why you are using compositionLocalOf in this way.

Using the State hoisting pattern you can use two parameters in to the composable:

  • value: T: the current value to display.
  • onValueChange: (T) -> Unit: an event that requests the value to change where T is the proposed new value.

In your case:

data class GlobalState(var count: Int = 0)

@Composable
fun App() {

    var counter by remember { mutableStateOf(GlobalState(0)) }
    CountPage(
        globalState = counter,
        onUpdateCount = {
                counter = counter.copy(count = counter.count +1)
            }
        )
}

@Composable
fun CountPage(globalState: GlobalState, onUpdateCount: () -> Unit) {
   
    BoxWithConstraints(
        contentAlignment = Alignment.Center,
        modifier = Modifier
            .fillMaxSize()
            .clickable (
                onClick = onUpdateCount
            )) {
        Text("count ${globalState.count}")
    }
}
Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
  • 2
    Thanks for your reply first. Forgive me didn't clearly describe my problem again. In my actual application, between `App` and `CountPage`, there are many Composable. Adding two parameters in those Composables seems is not a good way, because those Composable don't need to know this logic (just pass state down and event up) and would add a lot of code. So I think I should use CompositionLocal. – wjploop May 19 '21 at 01:36
  • @wjploop It is not clear what you are trying to do. In your case you should use a ViewModel, but the pattern doesn't change. – Gabriele Mariotti May 19 '21 at 06:26
  • One minor nit, you should change the `var` in `GlobalState` to a `val` as `GlobalState` should be immutable. – chuckj Jul 23 '21 at 23:37
3

You can declare your data as a MutableState and either provide separately the getter and the setter or just provide the MutableState object directly.

internal val LocalTest = compositionLocalOf<Boolean> { error("lalalalalala") }
internal val LocalSetTest = compositionLocalOf<(Boolean) -> Unit> { error("lalalalalala") }

@Composable
fun TestProvider(content: @Composable() () -> Unit) {
  val (test, setTest) = remember { mutableStateOf(false) }

  CompositionLocalProvider(
    LocalTest provides test,
    LocalSetTest provides setTest,
  ) {
    content()
  }
}

Inside a child component you can do:

@Composable
fun Child() {
  val test = LocalTest.current
  val setTest = LocalSetTest.current

  Column {
    Button(onClick = { setTest(!test) }) {
      Text(test.toString())
    }
  }
}
Silas Pedrosa
  • 196
  • 1
  • 4