I'm currently learning about the new Android stack (MVVM, compose, kotlin Flow/StateFlow), and I'm having trouble debugging a StateFlow where the value is updated, but I have no sign of collection from the composable.
It's a generic question, but I didn't find any solution to my problem by searching on my own.
Does anybody have an idea about what could disturb a StateFlow? I'm letting my code below:
ViewModel:
@HiltViewModel
class AuthViewModel @Inject constructor(
private val navigationManager: NavigationManager,
private val interactor: AuthInteractor
): BaseViewModel() {
companion object {
val TAG: String = AuthViewModel::class.java.simpleName
}
private val _uiState = MutableStateFlow(AuthenticationState())
val uiState: StateFlow<AuthenticationState> = _uiState
fun handleEvent(event: AuthenticationEvent) {
Log.v(TAG, "new event: $event")
when (event) {
is AuthenticationEvent.GoToRegistering -> navigateToRegistering()
is AuthenticationEvent.Register -> registerAccount(event)
is AuthenticationEvent.SnackbarMessage -> showSnackBar(event.message, event.type)
}
}
private fun navigateToRegistering() {
navigationManager.navigate(NavigationDirections.Authentication.registering)
}
private fun registerAccount(event: AuthenticationEvent.Register) {
Log.v(TAG, "register account")
_uiState.value.build {
isLoading = true
}
viewModelScope.launch {
Log.v(TAG, "launching request")
val exceptionMessage = interactor.registerUser(event.login, event.password)
Log.v(TAG, "response received, launching state from viewmodel")
_uiState.value.build {
exceptionMessage?.let {
Log.v(TAG, "Exception is not null")
isFailure = true
snackbarMessage = interactor.selectRegisterError(it)
}
}
}
}
}
UI Component:
@Composable
fun Authentication(
modifier: Modifier = Modifier,
viewModel: AuthViewModel,
type: AuthenticationType
) {
val state by viewModel.uiState.collectAsState()
Log.v("Authentication", "new state: $state, type = $type")
BackHandler(enabled = true) {
viewModel.handleEvent(AuthenticationEvent.Back)
}
state.snackbarMessage?.let { resourceId ->
val message = stringResource(resourceId)
Log.e("Authentication", "error: $message")
viewModel.handleEvent(AuthenticationEvent.SnackbarMessage(message, SnackbarType.ERROR))
}
if (state.isAuthenticated) {
// TODO: launch main screen
}
LaunchedEffect(key1 = state.isRegistered) {
// TODO: launch login event
}
AuthenticationContent(
modifier = modifier,
type = type,
viewModel = viewModel,
state = state
)
}
State Data Class:
data class AuthenticationState(
val isLoading: Boolean = false,
val isFailure: Boolean = false,
val isRegistered: Boolean = false,
val isAuthenticated: Boolean = false,
@StringRes val snackbarMessage: Int? = null
) {
fun build(block: Builder.() -> Unit) = Builder(this).apply(block).build()
class Builder(uiModel: AuthenticationState) {
var isLoading = uiModel.isLoading
var isFailure = uiModel.isFailure
var isRegistered = uiModel.isRegistered
var isAuthenticated = uiModel.isAuthenticated
var snackbarMessage = uiModel.snackbarMessage
fun build(): AuthenticationState {
return AuthenticationState(
isLoading,
isFailure,
isRegistered,
isAuthenticated,
snackbarMessage
)
}
}
}
CodePudding user response:
You are doing the updates like this:
_uiState.value.build {
isLoading = true
}
This code takes current value of uiState
and calls build function on it. Build function creates new instance of AuthenticationState
, but you don't do anything with that new instance and it's discarded. You have to set this new instance to be the new value of your StateFlow, like this:
_uiState.value = _uiState.value.build {}
// or this:
_uiState.update { current -> current.build {} }