pjp94
pjp94

Reputation: 75

Composable Not Collecting Updated Value in StateFlow

I have a form screen written in Compose where the user can fill out some TextFields and submit it. If they press the submit button before filling out the fields, the supporting text for the empty TextFields will change to say "Required".

Required text on empty fields

To do this, I've created a ViewState that includes a showRequired field:

data class ViewState(
    showRequired: Boolean = false,
    // other fields
)

In my view model, I create a MutableStateFlow with the ViewState as its default value:

private val _viewState: MutableStateFlow<ViewState> = MutableStateFlow(ViewState())
val viewState: StateFlow<viewState> = _viewState.asStateFlow()

To have the composable show the required message, I update the view state like this:

fun onSubmitClicked(form: Form) {
    if (areRequiredFieldsMissing(form)) {
        _viewState.update { it.copy(showRequired = true) }
    }
}

In my composable, I'm collecting that flow with collectAsStateWithLifecycle() and then checking if showRequired = true before showing the message:

val viewState = viewModel.viewState.collectAsStateWithLifecycle()

if (viewState.value.showRequired) {
    // set supportingText = Required
}

When I click the submit button, the message shows as expected. However, if I leave this screen and then come back to it, the required message shows by default instead of being hidden. I thought I could fix this by updating the view state when the back button is pressed to set showRequired = false:

// in my view model
fun onBackPressed() {
    _viewState.update { it.copy(showRequired = false) }
}

However this doesn't fix anything. When I come back to the form screen, the required message still shows by default. When I debug the composable, the value collected for showRequired is still true. Why doesn't updating it to false work correctly, but updating it to true when clicking the submit button does? I think I'm misunderstanding how StateFlow works but I'm not sure what to try.

EDIT: I resolved this by resetting my ViewState when exiting the screen so that showRequired is reset to false, instead of just updating it.

_viewState.value = ViewState()

Upvotes: 2

Views: 2243

Answers (2)

Abhimanyu
Abhimanyu

Reputation: 14877

This is my understanding of the use case.
Add a comment if it is not exactly the same.

You have a screen with multiple TextFields and a Submit Button. On clicking the submit button, the app navigates to a different screen.
The states are maintained in a shared ViewModel between screens. The error message is to be shown only after the user clicks on the button, but validation fails.

My first suggestion would be to use screen level ViewModel instead of shared ViewModel which automatically clears the states when it is removed.

But, there may be scenarios where that is not possible. In that case, you can try this,

ViewModel

// To store the state of the TextField input
private val _textFieldState: MutableStateFlow<String> = MutableStateFlow("")
val textFieldState: StateFlow<String> = _viewState.asStateFlow()

// To store the state of the CTA button click
private val _ctaClicked: MutableStateFlow<Boolean> = MutableStateFlow(false)
val ctaClicked: StateFlow<Boolean> = _viewState.asStateFlow()

// To store the state of the CTA button click
private val textFieldErrorState: MutableStateFlow<String?> = combine(
    textFieldState,
    ctaClicked,
) { textFieldState, ctaClicked, ->
    if (ctaClicked && textFieldState.isEmpty()) {
        "Required"
    } else {
        null
    }
}

We are using combine from Kotlin to listen to both the TextField input state as well as the CTA button click state.

You should reset the ctaClicked when navigating away from the screen.

Comment with more details, if this is not working for your use-case.

Upvotes: 0

yusufarisoy
yusufarisoy

Reputation: 91

showRequired should be a separate SharedFlow shouldn't keep in state as StateFlow, showing a message when there is an empty field on submit is an event not a state, you should show it only once. StateFlow is keeping its value and when you come back from a different screen recomposition occurs and

val viewState = viewModel.viewState.collectAsStateWithLifecycle()

if (viewState.value.showRequired) {
    // show message
}

this code block gets executed again, and because showRequired in state is set true message will be shown.

You should remove showRequired from your viewState

private val _formWarningEvent = MutableSharedFlow<Boolean>()
val formWarningEvent = _formWarningEvent.asSharedFlow()

in Composable

LaunchedEffect(Unit) {
    viewModel.formWarningEvent.onEach { show -> // can register lifecycle or use collect etc.
        if (show) // show message
    }
}

Upvotes: 0

Related Questions