Search code examples
androidkotlinandroid-recyclerviewkotlin-coroutinesandroid-viewmodel

Refactoring. Loading data in RecyclerView with MutableState from ViewModel


I have sealed class State and I need to load data from data class Loaded<T> to RecyclerView in Fragment. How to correctly observe state in fragment and load it to RecyclerView? The main problem, that data is generic.

sealed class State {
    object Success : State()
    object Waiting : State()
    object Loading : State()
    data class Loaded<T>(val data: T) : State()
    class Error(val message: String) : State()
    class TokenError(val message: String) : State() 
}

I've tried to use this code and it works fine, but I get warning Unchecked cast for val resorts = state.data as List<Resort> roads as List<Route>

override fun initFlow(){
    viewLifecycleOwner.lifecycleScope.launch {
        viewModel.state.collect { state ->
            when (state) {
                State.Waiting -> {
                    binding.progressBar.isVisible = false
                }
                State.Loading -> {
                    binding.progressBar.isVisible = true
                }
                State.Success -> {
                    binding.progressBar.isVisible = false
                }

                is State.Loaded<*> -> {
                    if (state.data is List<*>
                        && state.data.all { item -> item is Resort }
                    ) {
                        val resorts = state.data as List<Resort>
                        addResortsOnMap(resorts)
                    }

                    if (state.data is List<*>
                        && state.data.all { item -> item is Route }
                    ) {
                        val roads = state.data
                        roadsAdapter.setData(roads as List<Route>)
                    }
                    binding.progressBar.isVisible = false
                }

                is State.Error -> {
                    binding.progressBar.isVisible = false
                    Toast.makeText(
                        requireContext(),
                        state.message,
                        Toast.LENGTH_SHORT
                    ).show()
                }

                is State.TokenError -> {
                    Toast.makeText(requireContext(), state.message, Toast.LENGTH_SHORT).show()
                    findNavController().navigate(R.id.login)
                }
            }
        }
    }
}

I also tried to check in when, but it doesn't compile my app. I get error: Cannot check for instance of erased type:

is State.Loaded<List<Resort>> -> {}


Solution

  • You cannot check type using when operator, because types are erased at compile time and are substituted with Any. So during runtime, upon when gets executed, types are no longer known.

    If you want to get rid of "unchecked cast" warning, you can use as?:

    val resorts = state.data as? List<Resort>

    However in this case, you should handle possible failure during cast, which will result in resorts being assigned with null value.

    My suggestion is to rethink your state design. Generic list in State makes the architecture complicated, since you will be forced to implement type determining logic. I suppose you are trying to reuse one view for representing different data types, however at some point in your application flow you should know, what data you want to present.

    What I would do is to define two different states.

    class ResortListState : State() {
        val data: List<Resort>
    }
    
    class RouteListState : State() {
        val data: List<Route>
    }