Home > Blockchain >  How to use MutableStateFlow to search item in list
How to use MutableStateFlow to search item in list

Time:06-14

I am learning kotlin flow in android. I want to basically instant search in my list and filter to show in reyclerview. I searched in google and found this amazing enter image description here

Scenario 1

when I start typing in SearchBox and enter small a or capital A I want to show only two item matching in recyclerview, look like this

enter image description here

Scenario 2

when I enter any wrong text in SearchBox I want to basically show a text message that not found, look like this

enter image description here

Any guidance would be great. Thanks

I am adding my piece of code

ExploreViewModel.kt

class ExploreViewModel(private var list: ArrayList<Category>) : BaseViewModel() {

    val filteredTopics = MutableStateFlow<List<opics>>(emptyList())
    var topicSelected: TopicsArea? = TopicsArea.ALL
        set(value) {
            field = value
            handleTopicSelection(field ?: TopicsArea.ALL)
        }


    private fun handleTopicSelection(value: TopicsArea) {
        if (value == TopicsArea.ALL) {
            filterAllCategories(true)
        } else {
            filteredTopics.value = list.firstOrNull { it.topics != null && it.title == value.title }
                ?.topics?.sortedBy { topic -> topic.title }.orEmpty()
        }
    }

    fun filterAllCategories(isAllCategory: Boolean) {
        if (isAllCategory && topicSelected == TopicsArea.ALL && !isFirstItemIsAllCategory()) {
            list.add(0, code = TopicsArea.ALL.categoryCode))
        } else if (isFirstItemIsAllCategory()) {
            list.removeAt(0)
        }
        filteredTopics.value = list.flatMap { it.topics!! }.distinctBy { topic -> topic.title }.sortedBy { topic -> topic.title }
    }

    private fun isFirstItemIsAllCategory() = list.firstOrNull()?.code == TopicsArea.ALL
}

xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.SearchView
        android:id="@ id/searchView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="16dp"
        app:closeIcon="@drawable/ic_cancel"
        app:layout_constraintBottom_toTopOf="@ id/exploreScroll"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0"
        app:layout_constraintVertical_chainStyle="packed" />

    <HorizontalScrollView
        android:id="@ id/exploreScroll"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="10dp"
        android:scrollbars="none"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@ id/searchView">

        <com.google.android.material.chip.ChipGroup
            android:id="@ id/exploreChips"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:chipSpacingHorizontal="10dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:singleLine="true"
            app:singleSelection="true" />

    </HorizontalScrollView>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@ id/exploreList"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginBottom="20dp"
        android:paddingTop="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_default="wrap"
        app:layout_constraintVertical_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@ id/exploreScroll" />

</androidx.constraintlayout.widget.ConstraintLayout>

Category.kt

@Parcelize
data class Category(
    val id: String? = null,
    val title: String? = null,
    val code: String? = null,
    val topics: List<Topics>? = null,
) : Parcelable

Topics.kt

@Parcelize
data class Topics(
    val id: String? = null,
    val title: String? = null
) : Parcelable

Dummy data and coming from server

fun categoriesList() = listOf(
    Categories("21", "physical", listOf(Topics("1", "Abc one"), Topics("2", "Abc Two"))),
    Categories("2211", "mind", listOf(Topics("1", "xyz one"), Topics("2", "xyz two"))),
    Categories("22131", "motorized", listOf(Topics("1", "xyz three"), Topics("2", "xyz four"))),
)

In my view model list is holding above dummy data. And In my recyclerview I am passing the whole object and I am doing flatMap to combine all data into list. Make sure In recyclerview is using Topic and using title property. In Image Abc one, Abc two is holding in Topic. Thanks

After @Tenfour04 suggestion I will go to A2 suggestion because I have already data which converted into flow and passing in my adapter. I am adding my activity code as well.

ExploreActivity.kt

class ExploreActivity : AppCompatActivity() {

    private val binding by lazy { ExploreLayoutBinding.inflate(layoutInflater) }
    val viewModel by viewModel<ExploreViewModel> {
        val list = intent?.getParcelableArrayListExtra(LIST_KEY) ?: emptyList<Category>()
        parametersOf(list)
    }
    var exploreAdapter = ExploreAdapter { topic -> handleNextActivity(topic) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        setupView()
    }

    fun setupView() {
        setupSearchView()
        setupFilteredTopic()
        setupExploreAdapter()
    }

    private fun setupFilteredTopic() {
        lifecycleScope.launchWhenCreated {
            repeatOnLifecycle(Lifecycle.State.CREATED) {
                viewModel.filteredTopics.collect { filteredTopicsList ->
                    exploreAdapter.submitList(filteredTopicsList)
                }
            }
        }
    }

    fun setupSearchView() {
        binding.searchView.apply {
            setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(query: String?) = false
                override fun onQueryTextChange(newText: String?): Boolean {
                    return true
                }
            })
            
        }
    }

    fun setupExploreAdapter() {
        with(binding.exploreList) {
            adapter = exploreAdapter
        }
    }
}

UPDATE 2

ExploreViewModel.kt

 val filteredCategories = query
        .debounce(200) // low debounce because we are just filtering local data
        .distinctUntilChanged()
        .combine(filteredTopics) { queryText, categoriesList ->
            val criteria = queryText.lowercase()
            if (criteria.isEmpty()) {
                    return@combine filteredTopics 
            } else {
                categoriesList.filter { category -> category.title?.lowercase()?.let { criteria.contains(it) } == true }
            }
        }

I am getting error when I set in adapter

fixed

filteredTopics.value

enter image description here

CodePudding user response:

The tutorial you linked has a Flow produced by the SearchView. If you want to keep the search functionality in your ViewModel, you can put a MutableStateFlow in your ViewModel that will be updated by the SearchView indirectly. You can expose a property for updating the query.

There are two different ways this could be done, depending on whether you (A) already have a complete list of your data that you want to query quickly or (B) you want to query a server or your database every time your query text changes.

And then even (A) can be broken up into: (A1) you have a static plain old List, or (A2) your source List comes from a Flow, such as a returned Room flow that is not based on query parameters.

All code below is in the ViewModel class.

A1:

private val allCategories = categoriesList()
private val query = MutableStateFlow("")

// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
    get() = query.value
    set(value) { query.value = value }

// This is the flow that should be observed for the updated list that 
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
    .debounce(200) // low debounce because we are just filtering local data
    .distinctUntilChanged()
    .map { 
        val criteria = it.lowercase()
        allCategories.filter { category -> criteria in category.title.lowercase }
    }

A2:

In this example I put a simple placeholder flow for the upstream server query. This could be any flow.

private val allCategories = flow {
    categoriesList()
}
private val query = MutableStateFlow("")

// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
    get() = query.value
    set(value) { query.value = value }

// This is the flow that should be observed for the updated list that 
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
    .debounce(200) // low debounce because we are just filtering local data
    .distinctUntilChanged()
    .combine(allCategories) { queryText, categoriesList ->
        val criteria = queryText.lowercase()
        categoriesList.filter { category -> criteria in category.title.lowercase }
    }

B

private val query = MutableStateFlow("")

// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
    get() = query.value
    set(value) { query.value = value }

// This is the flow that should be observed for the updated list that 
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
    .debounce(500) // maybe bigger to avoid too many queries
    .distinctUntilChanged()
    .map { 
        val criteria = it.lowercase()
        categoriesList(criteria) // up to you to implement this depending on source
    }
  • Related