How to search a diffutil filter out results from existing list - kotlin

I'm using DiffUtil in my RecyclerView to displays a list from a database using the Room component. I would like to add a search function in the Appbar, that will filter out the existing items in the list as the user is typing.
My app currently has a search icon in the action bar, when you click the search icon it will expand across the Appbar and allow the user to search the database and return a new list. This mehtod involves querying the database each time.
Search Menu, This is where the parameters for the search widget are set.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="#+id/my_search"
android:title="Search"
android:icon="#drawable/ic_search"
app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>
RecyclerViewFragment
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.recycler_view_menu, menu)
val searchItem = menu.findItem(R.id.my_search)
val searchView: SearchView = searchItem.actionView as SearchView
searchView.imeOptions = EditorInfo.IME_ACTION_DONE
searchView.setIconifiedByDefault(false)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
// This approach queries my database for a new list.
viewModel.searchTopic("%$query%")
submitList()
return false
}
override fun onQueryTextChange(newText: String): Boolean {
// I would like to use the onTextChange() to filter out results from the list instead of querying a new list from the database.
return true
}
})
}
private fun submitList() {
viewModel.listDevTopics.observe(viewLifecycleOwner, Observer {
it?.let {
rvAdapter.submitList(it)
}
})
}
My RecyclerViewAdapter
class RecyclerViewAdapter() : androidx.recyclerview.widget.ListAdapter<Dev,
RecyclerViewAdapter.ItemViewHolder>(MyDiffCallback()) {
lateinit var searchList: List<Dev>
class MyDiffCallback : DiffUtil.ItemCallback<Dev>() {
override fun areItemsTheSame(oldItem: Dev, newItem: Dev): Boolean {
return oldItem.topic == newItem.topic
}
override fun areContentsTheSame(oldItem: Dev, newItem: Dev): Boolean {
return oldItem == newItem
}
}
class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
...
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
...
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
...
}
I would like to avoid querying the database every time for a search result, I want to use onQueryTextChange so it goes through the existing list and updates the list as the user is entering their query.

just implement Filterable and override getFilter Method
and make your filter object then return this object at getFilter Method
class JobOrderAdapter(val clickListener: JobOrderListener) : ListAdapter<CJO,
ViewHolder>(JobOrderDiffCallback()), Filterable {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder.from(parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item, clickListener)
}
private var list = listOf<CJO>()
fun setData(list: List<CJO>){
this.list = list
submitList(list)
}
override fun getFilter(): Filter = customFilter
private val customFilter = object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filteredList = mutableListOf<CJO>()
if (constraint == null || constraint.isEmpty()) {
filteredList.addAll(list)
} else {
val filterPattern = constraint.toString().toLowerCase().trim()
for (item in list) {
// here i am searching at custom obj by managerName
if (item.managerName.toLowerCase().contains(filterPattern)) {
filteredList.add(item)
}
}
}
val results = FilterResults()
results.values = filteredList
return results
}
override fun publishResults(constraint: CharSequence?, filterResults: FilterResults?) {
submitList(filterResults?.values as MutableList<CJO>?)
}
}}
and from your fragmnet or activity just call adapter.filter.filter(yourQueryText)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.home_menu, menu)
val searchByContract = menu.findItem(R.id.search_by_name)
val searchContractView = searchByContract.actionView as SearchView
searchContractView.queryHint = "البحث باسم مدير البيع"
searchContractView.inputType = InputType.TYPE_CLASS_TEXT
searchContractView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
adapter.filter.filter(newText)
return false
}
})
super.onCreateOptionsMenu(menu, inflater)
}

Related

RecycleView selection library - holding an item fires onSelectionChanged several times

I implemented a simple RecyclerView example, with multiple items selection possibility, using the selection library.
Here's my code:
class MainActivity : AppCompatActivity() {
private var listOfItems = ArrayList<Item>()
private var selectionTracker: SelectionTracker<Long>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.rvMainRecyclerView)
for (i in 0..32) {
listOfItems.add(Item(i, "Item $i"))
}
val recyclerViewAdapter = RecyclerViewAdapter(listOfItems)
recyclerView.adapter = recyclerViewAdapter
selectionTracker = SelectionTracker.Builder<Long>(
"Itemselection",
recyclerView,
MyItemKeyProvider(recyclerView),
MyItemDetailsLookup(recyclerView),
StorageStrategy.createLongStorage()
).build()
selectionTracker?.addObserver(
object: SelectionTracker.SelectionObserver<Long>() {
override fun onSelectionChanged() {
super.onSelectionChanged()
val itemsSelected = selectionTracker?.selection?.size()
Log.d("MainActivity", "$itemsSelected items selected")
}
}
)
recyclerViewAdapter.tracker = selectionTracker
}
}
class RecyclerViewAdapter(
val dataSet: ArrayList\<Item\>
): RecyclerView.Adapter\<RecyclerViewAdapter.ViewHolder\>() {
var tracker: SelectionTracker<Long>? = null
init {
setHasStableIds(true)
}
inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) {
val textView: TextView
init {
textView = view.findViewById(R.id.tvRecyclerViewItemTitle)
}
fun getItemDetails(): ItemDetails<Long> {
return object : ItemDetails<Long>() {
override fun getPosition(): Int {
return adapterPosition
}
override fun getSelectionKey(): Long? {
return itemId
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater
.from(parent.context).inflate(R.layout.recycler_view_item, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentItem = dataSet[position]
holder.textView.text = currentItem.value
holder.itemView.isActivated = tracker!!.isSelected(position.toLong())
}
override fun getItemCount(): Int {
return dataSet.size
}
override fun getItemId(position: Int): Long {
return dataSet[position].id.toLong()
}
}
I'm curious why the onSelectionChanged (in MainActivity selectionTracker?.addObserver...) is triggered several times while I hold an item to start selection mode?
To be more precise, this is the use case:
Nothing is selected,
Long click on any item => selection mode activated; the item is selected
Here, "1 items selected" is being printed out as long as I hold the first item.

Filter searchView from RecycleView with Adapter

Adapter class
class AppListAdapter(private val context: Context, initialChecked: ArrayList<String> = arrayListOf()) : RecyclerView.Adapter<AppListAdapter.AppViewHolder>() {
public val appList = arrayListOf<ApplicationInfo>()
private val checkedAppList = arrayListOf<Boolean>()
private val packageManager: PackageManager = context.packageManager
init {
context.packageManager.getInstalledApplications(PackageManager.GET_META_DATA).sortedBy { it.loadLabel(packageManager).toString() }.forEach { info ->
if (info.packageName != context.packageName) {
if (info.flags and ApplicationInfo.FLAG_SYSTEM == 0) {
appList.add(info)
checkedAppList.add(initialChecked.contains(info.packageName))
}
}
}
}
inner class AppViewHolder(private val item: ItemAppBinding) : RecyclerView.ViewHolder(item.root) {
fun bind(data: ApplicationInfo, position: Int) {
item.txApp.text = data.loadLabel(packageManager)
item.imgIcon.setImageDrawable(data.loadIcon(packageManager))
item.cbApp.isChecked = checkedAppList[position]
item.cbApp.setOnCheckedChangeListener { _, checked ->
checkedAppList[position] = checked
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
return AppViewHolder(ItemAppBinding.inflate(LayoutInflater.from(context), parent, false))
}
override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
holder.bind(appList[position], position)
}
override fun getItemCount(): Int {
return appList.size
}
on MainActivity
binding.searchView2.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
binding.searchView2.clearFocus()
// how to write code filtered by query?
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
// how to write code filtered by newText?
return false
}
})
I'm newbie in kotlin..anyone can help?
I believe you want to filter items with "ApplicationInfo.txApp", so i will write the code for it.
First you need your adapter class to extend Filterable like below, and add one more list to hold all items:
class AppListAdapter(private val context: Context, initialChecked: ArrayList<String> = arrayListOf()) : RecyclerView.Adapter<AppListAdapter.AppViewHolder>(), Filterable {
public val appList = arrayListOf<ApplicationInfo>()
public val appListFull = ArrayList<ApplicationInfo>(appList)
// This full list because of when you delete all the typing to searchView
// so it will get back to that full form.
Then override it's function and write your own filter to work, paste this code to your adapter:
override fun getFilter(): Filter {
return exampleFilter
}
private val exampleFilter = object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults? {
val filteredList: ArrayList<ApplicationInfo> = ArrayList()
if (constraint == null || constraint.isEmpty()) {
// when searchview is empty
filteredList.addAll(appListFull)
} else {
// when you type something
// it also uses Locale in case capital letters different.
val filterPattern = constraint.toString().lowercase(Locale.getDefault()).trim()
for (item in appListFull) {
val txApp = item.txApp
if (txApp.lowercase(Locale.getDefault()).contains(filterPattern)) {
filteredList.add(item)
}
}
}
val results = FilterResults()
results.values = filteredList
return results
}
#SuppressLint("NotifyDataSetChanged") #Suppress("UNCHECKED_CAST")
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
appList.clear()
appList.addAll(results!!.values as ArrayList<ApplicationInfo>)
notifyDataSetChanged()
}
}
And finally call this filter method in your searchview:
binding.searchView2.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
yourAdapter.filter.filter(query)
yourAdapter.notifyDataSetChanged()
binding.searchView2.clearFocus()
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
yourAdapter.filter.filter(newText)
yourAdapter.notifyDataSetChanged()
return false
}
})
These should work i'm using something similar to that, if not let me know with the problem.

Show item info on selected item in RecyclerView using Kotlin

I am using kotlin language with android studio. I want to get the properties of the element I clicked in the RecyclerView.
Ben bu kod ile saderc id alabiliyorum
Ex: date
ListAdapter.kt
class ListAdapter(
private val context: Context
) : RecyclerView.Adapter<ListAdapter.ListViewHolder>() {
private var dataList = mutableListOf<Any>()
private lateinit var mListener: onItemClickListener
interface onItemClickListener {
fun onItemClick(position: Int)
}
fun setOnItemClickListener(listener: onItemClickListener) {
mListener = listener
}
fun setListData(data: MutableList<Any>) {
dataList = data
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.item_row, parent, false)
return ListViewHolder(view)
}
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
val question: Questionio = dataList[position] as Questionio
holder.bindView(question)
}
override fun getItemCount(): Int {
return if (dataList.size > 0) {
dataList.size
} else {
return 0
}
}
inner class ListViewHolder(itemView: View, listener: onItemClickListener) :
RecyclerView.ViewHolder(itemView) {
fun bindView(questionio: Questionio) {
itemView.findViewById<TextView>(R.id.txt_policlinic).text = questionio.policlinic
itemView.findViewById<TextView>(R.id.txt_title).text = questionio.title
itemView.findViewById<TextView>(R.id.txt_description).text = questionio.description
itemView.findViewById<TextView>(R.id.txt_date).text = questionio.date
itemView.findViewById<TextView>(R.id.txt_time).text = questionio.time
}
init {
itemView.setOnClickListener {
listener.onItemClick(adapterPosition)
}
}
}
}
My code in onCreateView inside list fragment.Edit
ListFragment
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = adapter
observeData()
adapter.setOnItemClickListener(object : ListAdapter.onItemClickListener {
override fun onItemClick(position: Int) {
showShortToast(position.toString())
}
})
this function is also my observationData(),
I made new edits
private fun observeData() {
binding.shimmerViewContainer.startShimmer()
listViewModel.fetchQuestinData("questions",
requireContext())
.observe(viewLifecycleOwner, {
binding.shimmerViewContainer.startShimmer()
binding.shimmerViewContainer.hideShimmer()
binding.shimmerViewContainer.hide()
adapter.setListData(it)
adapter.notifyDataSetChanged()
})
}
You can pass highOrderFuction into the adapter then setonclickListener for any view you want. Like this:
class ListAdapter(
private val context: Context,
private val onItemClick:(questionio: Questionio)->Unit
) : RecyclerView.Adapter<ListAdapter.ListViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.item_row, parent, false)
return ListViewHolder(view,onItemClick)
}
...
inner class ListViewHolder(itemView: View,private val onItemClick:(questionio: Questionio)->Unit) : RecyclerView.ViewHolder(itemView) {
fun bindView(questionio: Questionio) {
//set on any view you want
itemView.findViewById<TextView>(R.id.root_view_id).
setOnClickListener{onItemClick(questionio)}
itemView.findViewById<TextView>(R.id.txt_policlinic).text =
questionio.policlinic
itemView.findViewById<TextView>(R.id.txt_title).text = questionio.title
itemView.findViewById<TextView>(R.id.txt_description).text =
questionio.description
itemView.findViewById<TextView>(R.id.txt_date).text = questionio.date
itemView.findViewById<TextView>(R.id.txt_time).text = questionio.time
}
}
}

RecyclerView AsyncListDiffer and data source consistency state lose with onClickListener

I have data source(in that example it's just a var myState: List)
class MainActivity : AppCompatActivity() {
var generation: Int = 0
var myState: List<User> = emptyList()
val userAdapter = UserAdapter {
val index = myState.indexOf(it)
if (index == -1)
println("🔥 not found")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recycler = findViewById<RecyclerView>(R.id.rvContent)
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = userAdapter
Thread {
while (true) {
generateNewData()
Handler(mainLooper).post {
userAdapter.submit(myState)
}
sleep(3000L)
}
}.start()
}
fun generateNewData() {
generation++
myState = (0..5000).map { User("$generation", it) }
}
}
I have RecyclerView, and AsyncListDiffer connected to it
data class User(val name: String, val id: Int) {
val createdTime = System.currentTimeMillis()
}
data class UserViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun bindTo(user: User, action: (User) -> Unit) {
val textView = view.findViewById<TextView>(R.id.title)
textView.text = "${user.name} ${user.id} ${user.createdTime}"
textView.setOnClickListener { action(user) }
}
}
class UserAdapter(val action: (User) -> Unit) : RecyclerView.Adapter<UserViewHolder>() {
val differ: AsyncListDiffer<User> = AsyncListDiffer(this, DIFF_CALLBACK);
object DIFF_CALLBACK : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false))
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = differ.currentList[position]
holder.bindTo(user, action = action)
}
fun submit(list: List<User>) {
differ.submitList(list)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
I have OnClickListener binded to every item on RecyclerView
{
val index = myState.indexOf(it)
if (index == -1)
println("🔥 not found")}
That listener checks if item that was clicked is exists in the data source, and if not, outputs it to the console.
Every few seconds data in the source are changed, and pushed to
a AsyncListDiffer via submitList method, some how internally it uses other thread to match data and pass that diffed data
to the RecyclerView, and that takes some time;
If I starts clicking on the items non-stop, and the click event occurs at the same time when the differ inserts new data, then I get into a non-consistent state.
So, how to handle that?
Ignore a click with inconsistent data?(cons: User can see some strange behaviour like list item not collapse/expand, no navigation happen, etc)
Try to find a similar item in the new data by separate fields(positions/etc), and use it?(cons: same as 1. but less probability)
Block OnClickListener events until the data is consistent in both the Recycler and the data source? (cons: same as above, and also lag with action user performed until data became consistent again)
Something else? What is a best way to solve that?

How can I convert this Firebase Recycler Adapter implementation into an Adapter.kt class?

I have written a Kotlin FirebaseRecyclerAdapter that works just fine as part of my MainActivity. However, I would like to have this code in a separate MainAdapter.kt file/class. How can I do this?
var query = FirebaseDatabase.getInstance()
.reference
.child("").child("categories")
.limitToLast(50)
val options = FirebaseRecyclerOptions.Builder<Category>()
.setQuery(query, Category::class.java)
.setLifecycleOwner(this)
.build()
val adapter = object : FirebaseRecyclerAdapter<Category, CategoryHolder>(options) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryHolder {
return CategoryHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.category_row, parent, false))
}
protected override fun onBindViewHolder(holder: CategoryHolder, position: Int, model: Category) {
holder.bind(model)
}
override fun onDataChanged() {
// Called each time there is a new data snapshot. You may want to use this method
// to hide a loading spinner or check for the "no documents" state and update your UI.
// ...
}
}
class CategoryHolder(val customView: View, var category: Category? = null) : RecyclerView.ViewHolder(customView) {
fun bind(category: Category) {
with(category) {
customView.textView_name?.text = category.name
customView.textView_description?.text = category.description
}
}
}
Given your code you could do something like this :
class MainAdapter(lifecycleOwner: LifecycleOwner) : FirebaseRecyclerAdapter<Category, CategoryHolder>(buildOptions(lifecycleOwner)) {
companion object {
private fun buildQuery() = FirebaseDatabase.getInstance()
.reference
.child("").child("categories")
.limitToLast(50)
private fun buildOptions(lifecycleOwner:LifecycleOwner) = FirebaseRecyclerOptions.Builder<Category>()
.setQuery(buildQuery(), Category::class.java)
.setLifecycleOwner(lifecycleOwner)
.build()
}
class CategoryHolder(val customView: View, var category: Category? = null) : RecyclerView.ViewHolder(customView) {
fun bind(category: Category) {
with(category) {
customView.textView_name?.text = category.name
customView.textView_description?.text = category.description
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryHolder {
return CategoryHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.category_row, parent, false))
}
protected override fun onBindViewHolder(holder: CategoryHolder, position: Int, model: Category) {
holder.bind(model)
}
override fun onDataChanged() {
// Called each time there is a new data snapshot. You may want to use this method
// to hide a loading spinner or check for the "no documents" state and update your UI.
// ...
}
}
There are many other ways to handle this problem, this is just an encapsulated version of yours.