Search code examples
androidandroid-jetpack-composekotlin-stateflow

Jetpack Compose passing LiveData to a Composable lambda


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.


Solution

  • 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) }
        )
    }