Currently I've this approach in my app:
ViewState(one viewState to each screen)
sealed class CategoriesViewState {
object Loading : CategoriesViewState()
data class Error(
val errorMessage: String,
val messageType: UIComponentType
) : CategoriesViewState()
data class CategoryList(
val categories: List<Category>
) : CategoriesViewState()
}
And I observe this state in my fragments/activites using live data:
viewModel.viewState.observe(viewLifecycleOwner, Observer {
when (it) {
is CategoriesViewState.Loading -> {
progress_bar.visibility = View.VISIBLE
Log.d(TAG, "LOADING")
}
is CategoriesViewState.Error -> {
progress_bar.visibility = View.GONE
Log.d(TAG, "ERROR")
}
is CategoriesViewState.CategoryList -> {
progress_bar.visibility = View.GONE
Log.d(TAG, "DATA")
}
}
})
And it is working fine.
BUT it seems to me inefficient as the app grows.
Let's say I've 20 screens in my app: I'll need 20 viewStates, I'll need to write the same when statement in every screen, I'll need to write this ugly Visible/Gone in every screen(not to mention I need to set Loading state in every call)
Maybe I'm totally wrong and it's common approach, But to me it seems like A LOT of code duplication.
I haven't a specific question, I Just wanna know if it is common approach in Android Development and if not, what am I doing wrong in my code?
Regarding your states for different activites, you don't need to make it every time you make new screen. You can follow approach like below and modify accordingly:
sealed class UIState<out T> where T : Any? {
object Loading : UIState<Nothing>()
data class Success<T>(val data: T) : UIState<T>()
data class Failure(val errorMessage: String, val messageType: UIComponentType) : UIState<Nothing>()
}
So, now your CategoriesViewState
can be represented as UiState<List<Category>>
.
I'd also created some extension functions to make things easier on observe:
infix fun <T> UIState<T>.takeIfSuccess(onSuccess: UIState.Success<T>.() -> Unit): UIState<T> {
return when (this) {
is UIState.Success -> {
onSuccess(this)
this
}
else -> {
this
}
}
}
infix fun <T> UIState<T>.takeIfError(onError: UIState.Failure.() -> Unit): UIState<T> {
return when (this) {
is UIState.Failure -> {
onError(this)
this
}
else -> {
this
}
}
}
And during observe method of live data:
viewModel.viewState.observe(viewLifecycleOwner) { state ->
state takeIfSuccess {
// Here's the success state
} takeIfError {
// Here's the error state
}
}
Edit:
If you don’t want to end up writing diamond brackets (<>
), here's the way to use type alias;
// for UIState of UserData class you can do something like this,
typealias UserDataState = UIState<UserData>
...
// Then use this typealias where you should be writing UIState, I.e.
val userLiveData = MutableLiveData<UserDataState>(value)