androidkotlinandroid-jetpack-composeviewmodelmutablestateflow

Ui does not recompose upon updating the stateflow from the ViewModel


I can change my view model's internal state by calling redirection(verified with Log statements), but it doesn't have any effect on gUiState in my main composable. Why is that?

The main composable supposed to manage my screens:

@Composable
fun BigBrother(gViewModel: GViewModel = viewModel()) {
    val gUiState by gViewModel.uiState.collectAsState()

    Log.d(TAG, "Chosen path is ${gUiState.direction}")

    when (gUiState.direction) {
        "START" -> StartScreen()
        "CHOOSER" -> Chooser()
        "GALLERY" -> ScrollItems(catalogue = gUiState.catalogItem)
        else -> StartScreen()
    }
}

@Composable
fun StartScreen(modifier: Modifier = Modifier) {
    Log.d(TAG, "Launching START")

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
    ) {
        Button(onClick = {
            GViewModel().redirection("CHOOSER")
            Log.d(TAG, "Button1 clicked, launching CHOOSER")
        }) {
            Text("New")
        }

        Button(onClick = {
            GViewModel().redirection("GALLERY")
            Log.d(TAG, "Button2 clicked, launching GALLERY")
        }) {
            Text("Browse")
        }
    }
}

UI State:

data class GUiState(
    val item: Item = Item(),
    val catalogItem: List<Item> = listOf(Item()),
    val direction: String = "START",
    val parameterChange: Parameter = Parameter("New", 14),
)

View model:

class GViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(GUiState())
    val uiState: StateFlow<GUiState> = _uiState.asStateFlow()

    // ....

    fun redirection(newDirection: String) {
        _uiState.update { currentState -> currentState.copy(direction = newDirection) }
        Log.d(TAG, "changed UIstate direction to ${uiState.value.direction}")
    }
}

To clarify the result, I can see the message from the redirection function in the logcat, thus confirming the update of the _uiState, but then I never see another log message from the main screen about the new choice for the path.


Solution

  • In StartScreen you do the following:

    GViewModel().redirection("CHOOSER")
    

    This creates a new GViewModel instance (with GViewModel()) that you use to set the state by calling redirection.

    That's not what you want: You want to change the already existing view model instance because only that is observed for changes. To accomplish that you need to pass StartScreen the function that should be called:

    @Composable
    fun StartScreen(
        redirection: (String) -> Unit,
        modifier: Modifier = Modifier,
    ) {
        // ...
    
        Button(onClick = { redirection("CHOOSER") }) {
        }
    
        // ...
    }
    

    You can then pass the function like this:

    @Composable
    fun BigBrother(gViewModel: GViewModel = viewModel()) {
        val gUiState by gViewModel.uiState.collectAsState()
    
        when (gUiState.direction) {
            "START" -> StartScreen(redirection = gViewModel::redirection)
            // ...
        }
    }
    

    So whenever your StartScreen calls redirection, what actually is getting executed is redirection from your BigBrother's gViewModel object. And since this object's uiState is observed for changes, now it will actually have an effect.