2

I'm having trouble understanding why my value I'm observing as state is properly passed into a Composable lambda, but does not trigger that composable to recompose.

Here's my setup.

Screen

@Composable
fun Screen(
    showBottomSheet: (@Composable () -> Unit) -> Unit,
    viewModel: MyViewModel = hiltViewModel()
) {
    val someValue = viewModel.someValue.observeAsState().value

    MyController(
        onShowBottomSheetClick = {
            showBottomSheet {
                Log.d("DEBUG", "value updated: $someValue")
                BottomSheetLayout(
                    someValue = someValue,
                    onUpdateClick = { newValue -> viewModel.setValue(newValue) }
                )
            }
        }
    )
}

MyController

@Composable
fun MyController(
    onShowBottomSheetClick: () -> Unit
) {
    Text(
        text = "show BottomSheet",
        modifier = Modifier.clickable { onShowBottomSheetClick() }
    )
}

BottomSheetLayout

@Composable
fun BottomSheetLayout(
    someValue: Int,
    onUpdateClick: (Int) -> Unit
) {
    Text(
        text = "Some value: $someValue",
        modifier = Modifier.clickable { onUpdateClick(someValue + 1) }
    )
}

MyViewModel

@HiltViewModel
class MyViewModel @Inject constructor(): ViewModel() {
    private val _someValue: Int? = MutableLiveData(null)
    val someValue: LiveData<Int?> = _someValue

    fun setValue(newValue: Int) {
        _someValue.value = newValue
    }
}

When I run this and see output, the log doesn't show updated someValue.

value updated: 0
value updated: 0
value updated: 0

However, if I add a new state value to the lambda itself, it work fine.

// Screen
// ..
    showBottomSheet {
        val currentValue = viewModel.someValue.observeAsState().value // <-- Note the difference
        Log.d("DEBUG", "value updated: $currentValue")
        BottomSheetLayout(
            someValue = currentValue,
            onUpdateClick = { newValue -> viewModel.setValue(newValue) }
        )
    }
// ..

When I run this and see output, the log shows properly updated value.

value updated: 0
value updated: 1
value updated: 2

Why is this happening? Is there any way I can pass the first state value to the showBottomSheet composable lambda?

Thank you in advance.

Saehun Sean Oh
  • 2,103
  • 1
  • 21
  • 41

3 Answers3

1

I haven't tried your code but this looks like an effect of scoped recomposition as in this question. In that question even without remember mutableState value is updated because outer scope wasn't recomposed. In your case since you are passing value instead of state that is not updated because of not triggering recomposition.

Why does mutableStateOf without remember work sometimes?

@Composable
fun Screen(
    showBottomSheet: (@Composable () -> Unit) -> Unit,
    viewModel: MyViewModel = hiltViewModel()
) {
    val someValue = viewModel.someValue.observeAsState().value

    MyController(
        onShowBottomSheetClick = {
            showBottomSheet {
                Log.d("DEBUG", "value updated: $someValue")
                BottomSheetLayout(
                    someValue = someValue,
                    onUpdateClick = { newValue -> viewModel.setValue(newValue) }
                )
            }
        }
    )
}

In this code only showBottomSheet is recomposed and because of that someValue which is a value is not updated since there is no recomposition in MyController scope.

With this code, i assume showBottomSheet is a Composable since you are able to call @Composable function Flow.collectAsState you are recomposing everything inside and that's why it's recomposed with lates value of viewModel.someValue

showBottomSheet {
    val currentValue = viewModel.someValue.observeAsState().value // <-- Note the difference
    Log.d("DEBUG", "value updated: $currentValue")
    BottomSheetLayout(
        someValue = currentValue,
        onUpdateClick = { newValue -> viewModel.setValue(newValue) }
    )
}
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • This must be it. I didn't know such thing could impact the recompose process. Thank you for your answer and for linking another question that explained the very feature of Jetpack Compose. – Saehun Sean Oh Sep 26 '22 at 22:56
0

Log.d is a Side-Effect and should be used with one of its handler.

read more about Jetpack Compose Side-Effects

Use LaunchedEffect or DisposableEffect

@Composable
fun Screen(
    showBottomSheet: (@Composable () -> Unit) -> Unit,
    viewModel: MyViewModel = hiltViewModel()
) {
    val someValue = viewModel.someValue.observeAsState().value
    
    LaunchedEffect(someValue) {
        Log.d("DEBUG", "value updated: $someValue")
    }

    MyController(
        onShowBottomSheetClick = {
            showBottomSheet {
                BottomSheetLayout(
                    someValue = someValue,
                    onUpdateClick = { newValue -> viewModel.setValue(newValue) }
                )
            }
        }
    )
}

The effect will be started every time your value changes

Merkost
  • 1,251
  • 9
  • 16
  • I'm not asking how to printout log message properly. I was expecting BottomSheetLayout Composable to recompose because there was a change in `someValue`, but it's not. – Saehun Sean Oh Sep 16 '22 at 10:36
0

Use the Delegate

That wraps your value to State<T> the same way as remember API does, thus recomposition will be caused every time your value changes.

val someValue by viewModel.someValue.observeAsState()

In your example the state immediately transformed because of .value and recomposition was not called.

Merkost
  • 1,251
  • 9
  • 16