A question about UiState: using the Loading, Error...
# compose
p
A question about UiState: using the Loading, Error, Success UiState sealed interface pattern, what should we do if we need to add a lot of things, for example, a dialogOnScreen variable, a dialog text variable, etc... how can we add it here?
Copy code
sealed interface MyModelUiState {
    object Loading : MyModelUiState
    data class Error(val throwable: Throwable) : MyModelUiState
    data class Success(val data: List<String>) : MyModelUiState
}
checking compose samples I can see that they don't use this patter when they need to add more content, instead, they do this:
Copy code
data class ReplyHomeUIState(
    val emails: List<Email> = emptyList(),
    val selectedEmails: Set<Long> = emptySet(),
    val openedEmail: Email? = null,
    val isDetailOnlyOpen: Boolean = false,
    val loading: Boolean = false,
    val error: String? = null
)
How to modify MyModelUiState to for example add a dialogOnScreen variable and a dialog text variable? Should these variables be inside "Success" data class as parameters? or should I stop using the sealed interface pattern and do the other style I showed in the second sample?
p
Just as you described, if your screen presents a different structure of the View, that sounds like a new State, "UiState.DialogMode(dialogData: DialogData)" Or to be honest in the case of dialog I would create as a separate screen state let's say:
Copy code
sealed class DialogState {
  class Present(dialogData: DialogData):DialogState
  object Empty: DialogState// when no dialog
 }
Normally the rule I follow is, if the UI structure changes a lot, let's say a loader vs a List, then I use a seal class to represent each structure. If the change is in style and color and a few hide/show elements, then I just change the data to paint but no need a new UiState class. Just different values for the data in that state
p
well I feel like my question is not answered, but I understand you
will my sample be correct like this?
Copy code
sealed interface BusStopsDBScreenUiState {
    object Loading : BusStopsDBScreenUiState
    data class Error(val throwable: Throwable) : BusStopsDBScreenUiState
    data class Success(
        val data: List<BusStop>,
        val dialogText: StringResource?
    ) : BusStopsDBScreenUiState
}
simply setting StringResource to a non null or null value will determine if dialog must be shown
is that a correct approach?
p
I think it will work yes. Putting all UI data in one single state class like the second approach you posted, it makes harder to consuming the state from Composable functions. Better to split the data into the state it belongs to. Using the sealed class technique. The Success class above, you can even split it in more substates, but in general what you post I think is ok.
Like I said, the case of the dialog is a bit different, although you can still can put thendialog data in the Success. I would have it as part of another sealed UI state class. The purpose is keeping the existing UI state in the background whilst the dialog is displayed
p
Thanks, on the other hand now I have another issue with this. I'm setting the UiState using a flow which reads from the repository:
Copy code
val uiState: StateFlow<BusStopsDBScreenUiState> = busDataRepository.getBusStops()
    .map<List<BusStop>, BusStopsDBScreenUiState> { busStops ->
        BusStopsDBScreenUiState.Success(busStops, Res.string.bus_stops_db_filled)
    }
    .catch { throwable ->
        emit(BusStopsDBScreenUiState.Error(throwable))
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BusStopsDBScreenUiState.Loading)

fun updateBusStops() {
    viewModelScope.launch(Dispatchers.Main) {
        launch(<http://Dispatchers.IO|Dispatchers.IO>) {
            busDataRepository.updateBusStops()
        }
    }
}
now, I need a method to hide the dialog, simply setting the dialogText to null on the Success state of the UiState, but... how can I set that to null?
I'm very newbie with flows, I still don't feel good using them, and for example don't know how to do this
I know that with that code, flows will be emitted and updated automatically as the updateBusStops method is called, but because it is automatic, now I don't know how to modify just that variable
p
I assume this code is part of a ViewModel or StateController class. I would use the pattern where you expose a StateFlow to the public but keep a private MutateStateFlow. Then from a function in this class, you just update the state to the new value
There are a lot of examples doing what I described
p
well It's very complex to understand for me
can't find this exact thing in any sample for now
and yes, is in the viewmodel
p
Sort of:
Copy code
private val _uiStateFlow: MutableStateFlow<UiState> = createInitialMutableStateFlow()
val uiStateFlow: = _uiStateFlow.asState()

fun updateUiState {
  _uiStateFlow.update {
    it.copy(propertyXYZ = newValu)
  }
}
I am on the phone so probably some error but that's the general idea
p
that doesn't work, because the initial creation of the flow returns a state flow, not a mutable one:
Copy code
Type mismatch.
Required:
MutableStateFlow<BusStopsDBScreenUiState>
Found:
StateFlow<BusStopsDBScreenUiState>
stateIn returns a StateFlow
p
I see, can't you change it?
Perhaps move stateIn to the public one
p
don't understand
d
You'd want another (private) MutableStateFlow in your ViewModel that holds the dialog state data and then you'd combine for your uiState. Sorry I'm on my phone so can't quite give you a code snippet
☝️ 1