'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 |