androidkotlinmvvmandroid-livedataandroid-architecture-components

How to handle View State in Android development in the right way


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?


Solution

  • 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)