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
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
Scenario 2
when I enter any wrong text in SearchBox I want to basically show a text message that not found, look like this
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 doingflatMap
to combine all data into list. Make sure In recyclerview is usingTopic
and usingtitle
property. In ImageAbc one
,Abc two
is holding inTopic
. 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
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
}