'Filtering a recyclerview list using map or switchmap

I've been trying to figure this out for 2 days now - I just can't seem to get it to work!

I'm using MVVM with a Repository pattern.

Could someone tell me what I'm doing wrong here? I'm trying to filter the list to show characters who appeared in specific seasons e.g any characters from season 2 would be displayed but characters who weren't in season 2 would be omitted from being displayed. The season list is also from the api endpoint which is why I'm trying to filter it in the viewmodel.

Is this the right way to do this or is there a better/different way to do it ?

Here's my Fragment class

@AndroidEntryPoint
class CharactersFragment : Fragment(R.layout.fragment_character_list) {

    private lateinit var binding: FragmentCharacterListBinding
    private val recyclerViewAdapter = MyCharactersRecyclerViewAdapter()
    private val viewModel: CharactersViewModel by viewModels()


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentCharacterListBinding.inflate(inflater, container, false)
        setHasOptionsMenu(true)

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        showError()
        setupRecyclerView()
        navigateToDetails()

    }

    override fun onOptionsItemSelected(item: MenuItem) =

        when (item.itemId) {
            R.id.menu_filter -> {
                showFilteringPopUpMenu()
                true
            }
            else -> {
                false
            }
        }


    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.characters_fragment_menu, menu)

        val searchItem: MenuItem = menu.findItem(R.id.menu_item_search)
        val searchView = searchItem.actionView as SearchView

        searchView.apply {

            setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(queryText: String): Boolean {
                    Log.d("MainActivity", "QueryTextSubmit: $queryText")
                    return false
                }

                override fun onQueryTextChange(queryText: String): Boolean {
                    Log.d("MainActivity", "QueryTextChange: $queryText")
                    recyclerViewAdapter.filter.filter(queryText)
                    return true
                }
            })
        }
    }

    private fun navigateToDetails() {
        recyclerViewAdapter.setOnItemClickListener {
            val action =
                CharactersFragmentDirections.actionCharactersFragmentToCharacterDetailsFragment(it)
            findNavController().navigate(action)
        }
    }

    private fun setupRecyclerView() {
        binding.list.apply {
            layoutManager =
                StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
            adapter = recyclerViewAdapter

            viewModel.characters.observe(viewLifecycleOwner) {
                it?.let {
                    recyclerViewAdapter.updateList(it)
                    Log.d("TAG", "onViewCreated: ${it.size}")
                }
            }
        }
    }

    private fun showError() {
        viewModel.spinner.observe(viewLifecycleOwner) { value ->
            value.let { show ->
                binding.spinner.visibility = if (show) View.VISIBLE else View.GONE
            }
        }
        viewModel.errorText.observe(viewLifecycleOwner) { text ->
            text?.let {
                binding.errorTextView.apply {
                    this.text = text
                    visibility = View.VISIBLE
                }
                binding.list.visibility = View.GONE
                viewModel.onErrorTextShown()
            }
        }
    }

    private fun showFilteringPopUpMenu() {
        val view = activity?.findViewById<View>(R.id.menu_filter) ?: return
        PopupMenu(requireContext(), view).run {
            menuInflater.inflate(R.menu.filter_seasons, menu)


            setOnMenuItemClickListener {

                when (it.itemId) {
                    R.id.one -> {
                        viewModel.season.value = FilterSeasons.SEASON_ONE
                        Toast.makeText(requireContext(), "Season One", Toast.LENGTH_SHORT)
                            .show()
                    }
                    R.id.two -> {
                        viewModel.season.value = FilterSeasons.SEASON_TWO
                        Toast.makeText(requireContext(), "Season Two", Toast.LENGTH_SHORT)
                            .show()
                    }
                    R.id.three -> {
                        viewModel.season.value = FilterSeasons.SEASON_THREE
                        Toast.makeText(requireContext(), "Season Three", Toast.LENGTH_SHORT)
                            .show()
                    }
                    R.id.four -> {
                        viewModel.season.value = FilterSeasons.SEASON_FOUR
                        Toast.makeText(requireContext(), "Season Four", Toast.LENGTH_SHORT)
                            .show()
                    }
                    R.id.five -> {
                        viewModel.season.value = FilterSeasons.SEASON_FIVE
                        Toast.makeText(requireContext(), "Season Five", Toast.LENGTH_SHORT)
                            .show()
                    }
                    else -> {
                        viewModel.season.value = FilterSeasons.ALL_SEASONS
                        Toast.makeText(requireContext(), "All Seasons", Toast.LENGTH_SHORT)
                            .show()
                    }
                }
                true
            }
            show()
        }
    }
}

And here's my ViewModel

@HiltViewModel
class CharactersViewModel @Inject constructor(
    private val repository: Repository
) : ViewModel() {

    private val _spinner = MutableLiveData<Boolean>(false)
    val spinner: LiveData<Boolean> = _spinner

    val season = MutableLiveData<FilterSeasons>()


    private val _errorText = MutableLiveData<String?>()
    val errorText: LiveData<String?> = _errorText

    private val _characters = MutableLiveData<List<BreakingBadCharacterItem>?>()
    val characters: LiveData<List<BreakingBadCharacterItem>?> =
        _characters.map { seasons ->

            when (season.value) {
                ALL_SEASONS -> {
                    seasons
                }
                SEASON_ONE -> seasons?.filter {
                    it.appearance.any { it == 1 }
                }
                SEASON_TWO -> seasons?.filter {
                    it.appearance.any { it == 2 }
                }
                SEASON_THREE -> seasons?.filter {
                    it.appearance.any { it == 3 }
                }
                SEASON_FOUR -> seasons?.filter {
                    it.appearance.any { it == 4 }
                }
                SEASON_FIVE -> seasons?.filter {
                    it.appearance.any { it == 5 }
                }
                else -> {
                    seasons
                }
            }
        }


    init {
        getAllCharacters()
        season.value = ALL_SEASONS
    }


    private fun getAllCharacters() =
        viewModelScope.launch {
            try {
                _spinner.postValue(true)
                val response = repository.loadBBCharacters()
                _characters.postValue(response)
            } catch (error: BreakingError) {
                _errorText.postValue(error.message)
            } finally {
                _spinner.postValue(false)
            }
        }

    fun onErrorTextShown() {
        _errorText.value = null
    }

}

I also have an Enum class

enum class FilterSeasons {
    ALL_SEASONS,
    SEASON_ONE,
    SEASON_TWO,
    SEASON_THREE,
    SEASON_FOUR,
    SEASON_FIVE
}


Solution 1:[1]

In your menuItemListener you are modifying the viewmodel.season bit you're not actually listening/observing to any changes to this value.

I would recommend a custom setter here (not sure it needs to be liveData as it's not being observed) :

val season: FilterSeasons = FilterSeasons.All
set(value) {
field = value 
filterCharacterBySeason(value)
}

And then abstract the filtering you are doing in the character LiveData into itls own method filterCharacterBySeason(season: FilterSeason)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 gRaduateToaster