Android RecyclerView With SearchView
This article explains how to integrate searchview in toolbar to search data in recyclerview. We can use Filterable interface to sort data using some conditions.
We need to implement Filterable interface in adapter class. For this we must have to override getFilter method to sort data. In this method, We provide some conditions. So we can get specific data on these conditions.
In this example, First we fetch employees data from api. Then we filter data to search specific employees by name or username of employees.
Add below dependencies in your app level build.gradle file
We are using retrofit to fetch employees data from api. To display data, We need to add recyclerview and cardview libraries.
implementation ‘com.squareup.retrofit2:converter-gson:2.5.0’
implementation ‘com.squareup.okhttp3:logging-interceptor:3.9.1’
implementation ‘androidx.recyclerview:recyclerview:1.1.0’
implementation ‘androidx.cardview:cardview:1.0.0’
Add SearchView In Toolbar
Create Menu File to display SearchMenu in Toolbar
xmlns:app=”http://schemas.android.com/apk/res-auto”
xmlns:tools=”http://schemas.android.com/tools”
tools:context=”info.androidhive.recyclerviewsearch.MainActivity”>
<item
android:id=”@+id/action_search”
android:icon=”@android:drawable/ic_search_category_default”
android:title=”Search”
app:showAsAction=”always”
app:actionViewClass=”androidx.appcompat.widget.SearchView” />
</menu>
Create xml file under res->xml folder
<searchable xmlns:android=”http://schemas.android.com/apk/res/android”
android:hint=”search”
android:label=”@string/app_name” />
Changes in related activity in manifest file
android:name=”android.app.searchable”
android:resource=”@xml/searchable” />
<intent-filter>
<action android:name=”android.intent.action.SEARCH” />
</intent-filter>
Add Menu in Activity Toolbar
menuInflater.inflate(R.menu.menu_main, menu)
val mSearchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
mSearchView = menu?.findItem(R.id.action_search)?.actionView as SearchView
mSearchView?.setSearchableInfo(mSearchManager.getSearchableInfo(componentName))
mSearchView?.maxWidth = Int.MAX_VALUE
mSearchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
employeesAdapter?.filter?.filter(query)
return false
}
override fun onQueryTextChange(query: String?): Boolean {
employeesAdapter?.filter?.filter(query)
return false
}
})
return true
}
val id = item?.itemId
return if (id == R.id.action_search) {
true
} else super.onOptionsItemSelected(item)
}
Filter RecyclerView Data
return object : Filter() {
override fun performFiltering(charSequence: CharSequence): FilterResults {
if (charSequence.toString().isEmpty()) {
filteredListEmployees = listEmployees
} else {
val employeesFilteredList: MutableList<Employee> = ArrayList()
listEmployees?.let {
for (employee in it) {
employee.name?.let { mName ->
employee.userName?.let { mUserName ->
if (mName.toLowerCase().contains(charSequence.toString().toLowerCase())
|| mUserName.toLowerCase().contains(charSequence.toString().toLowerCase()) ) {
employeesFilteredList.add(employee)
}
}
}
}
}
filteredListEmployees = employeesFilteredList
}
val filterResults = FilterResults()
filterResults.values = filteredListEmployees
return filterResults
}
override fun publishResults( charSequence: CharSequence, filterResults: FilterResults ) {
filteredListEmployees = filterResults.values as MutableList<Employee>
notifyDataSetChanged()
}
}
}
Final Code
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.recyclerviewwithsearchfiltersample"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <meta-data android:name="android.app.searchable" android:resource="@xml/searchable" /> <intent-filter> <action android:name="android.intent.action.SEARCH" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
activity_main.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.recyclerview.widget.RecyclerView android:id="@+id/recycler_main" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
employee_row.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" app:cardCornerRadius="10dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:layout_marginStart="15dp" android:layout_marginEnd="15dp"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp"> <TextView android:id="@+id/txt_employee_name" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" /> <TextView android:id="@+id/txt_employee_info1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/txt_employee_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" /> <TextView android:id="@+id/txt_employee_info2" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/txt_employee_info1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" /> <TextView android:id="@+id/txt_employee_address" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/txt_employee_info2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.cardview.widget.CardView>
Employee.kt
package com.example.recyclerviewwithsearchfiltersample.models import com.google.gson.annotations.SerializedName import java.io.Serializable data class Employee( @SerializedName("id") val id: Int? = null, @SerializedName("name") val name: String? = null, @SerializedName("username") val userName: String? = null, @SerializedName("email") val email: String? = null, @SerializedName("address") val employeeAddress : EmployeeAddress? = null, @SerializedName("phone") val phone: String? = null, @SerializedName("website") val website: String? = null ) : Serializable
EmployeeAddress.kt
package com.example.recyclerviewwithsearchfiltersample.models import com.google.gson.annotations.SerializedName import java.io.Serializable data class EmployeeAddress( @SerializedName("street") val street: String? = null, @SerializedName("suite") val suite: String? = null, @SerializedName("city") val city: String? = null, @SerializedName("zipcode") val zipCode: String? = null ) : Serializable
ApiClient.kt
package com.example.recyclerviewwithsearchfiltersample.utils import com.google.gson.Gson import com.google.gson.GsonBuilder import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory object ApiClient { private const val BASE_URL: String = "https://jsonplaceholder.typicode.com/" private val gson : Gson by lazy { GsonBuilder().setLenient().create() } private val interceptor : HttpLoggingInterceptor by lazy { HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) } private val httpClient : OkHttpClient by lazy { OkHttpClient.Builder().addInterceptor(interceptor).build() } private val retrofit : Retrofit by lazy { Retrofit.Builder() .baseUrl(BASE_URL) .client(httpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() } val apiService : ApiService by lazy{ retrofit.create(ApiService::class.java) } }
ApiService.kt
package com.example.recyclerviewwithsearchfiltersample.utils import com.example.recyclerviewwithsearchfiltersample.models.Employee import retrofit2.Call import retrofit2.http.GET interface ApiService { @GET("users") fun getUsers(): Call<MutableList<Employee>> }
Utility.kt
package com.example.recyclerviewwithsearchfiltersample.utils import android.app.Activity import android.content.Context import android.graphics.Color import android.net.ConnectivityManager import android.view.Gravity import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.RelativeLayout import android.widget.TextView import android.widget.Toast import com.example.recyclerviewwithsearchfiltersample.R object Utility { private var progressBar: ProgressBar? = null fun Context.isInternetAvailable(): Boolean { try { val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val netInfo = cm.activeNetworkInfo return if (netInfo != null && netInfo.isConnected) { true } else { showErrorToast("Internet not available. Please try again!!") false } } catch (e: Exception) { e.printStackTrace() } return false } fun Context.showErrorToast(message: String?) { try { val toast = Toast.makeText(this, message, Toast.LENGTH_LONG) val textView = toast.view.findViewById(android.R.id.message) as? TextView textView?.setTextColor(Color.WHITE) textView?.gravity = Gravity.CENTER toast.view.setBackgroundColor(Color.RED) toast.setGravity(Gravity.TOP or Gravity.FILL_HORIZONTAL, 0, 0) toast.show() } catch (e: Exception) { e.printStackTrace() } } fun Context.showProgressBar() { try { val layout = (this as? Activity)?.findViewById<View>(android.R.id.content)?.rootView as? ViewGroup progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleLarge) progressBar?.let { it1 -> it1.isIndeterminate = true val params = RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT ) val rl = RelativeLayout(this) rl.gravity = Gravity.CENTER rl.addView(it1) layout?.addView(rl, params) it1.visibility = View.VISIBLE } } catch (e: Exception) { e.printStackTrace() } } fun hideProgressBar() { try { progressBar?.let { it.visibility = View.GONE } } catch (e: Exception) { e.printStackTrace() } } }
EmployeesAdapter.kt
<pre class="wp-block-syntaxhighlighter-code"> package com.example.recyclerviewwithsearchfiltersample.adapter import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Filter import android.widget.Filterable import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.example.recyclerviewwithsearchfiltersample.R import com.example.recyclerviewwithsearchfiltersample.models.Employee class EmployeesAdapter(private val context: Context, private var listEmployees: MutableList<Employee>?) : RecyclerView.Adapter<EmployeesAdapter.MyViewHolder>(), Filterable { private var filteredListEmployees : MutableList<Employee>? = null init { this.filteredListEmployees = listEmployees } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { val inflater = LayoutInflater.from(context) val view: View = inflater.inflate(R.layout.employee_row,parent,false) return MyViewHolder(view) } override fun getItemCount(): Int { filteredListEmployees?.let { return it.size } return 0 } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { val employee = filteredListEmployees?.get(position) holder.txtEmployeeName?.text = employee?.name holder.txtEmployeeInfo1?.text = employee?.userName + " | " + employee?.email holder.txtEmployeeInfo2?.text = employee?.phone + " | " + employee?.website val employeeAddress = employee?.employeeAddress holder.txtEmployeeAddress?.text = employeeAddress?.suite + "," + employeeAddress?.street + "," + employeeAddress?.city + "," + employeeAddress?.zipCode } class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view){ var txtEmployeeName: TextView? = null var txtEmployeeInfo1: TextView? = null var txtEmployeeInfo2: TextView? = null var txtEmployeeAddress: TextView? = null init { txtEmployeeName = view.findViewById(R.id.txt_employee_name) txtEmployeeInfo1 = view.findViewById(R.id.txt_employee_info1) txtEmployeeInfo2 = view.findViewById(R.id.txt_employee_info2) txtEmployeeAddress = view.findViewById(R.id.txt_employee_address) } } override fun getFilter(): Filter { return object : Filter() { override fun performFiltering(charSequence: CharSequence): FilterResults { if (charSequence.toString().isEmpty()) { filteredListEmployees = listEmployees } else { val employeesFilteredList: MutableList<Employee> = ArrayList() listEmployees?.let { for (employee in it) { employee.name?.let { mName -> employee.userName?.let { mUserName -> if (mName.toLowerCase().contains(charSequence.toString().toLowerCase()) || mUserName.toLowerCase().contains(charSequence.toString().toLowerCase()) ) { employeesFilteredList.add(employee) } } } } } filteredListEmployees = employeesFilteredList } val filterResults = FilterResults() filterResults.values = filteredListEmployees return filterResults } override fun publishResults( charSequence: CharSequence, filterResults: FilterResults ) { filteredListEmployees = filterResults.values as MutableList<Employee> notifyDataSetChanged() } } } }</pre>
MainActivity.kt
package com.example.recyclerviewwithsearchfiltersample import android.app.SearchManager import android.content.Context import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.recyclerview.widget.LinearLayoutManager import com.example.recyclerviewwithsearchfiltersample.adapter.EmployeesAdapter import com.example.recyclerviewwithsearchfiltersample.models.Employee import com.example.recyclerviewwithsearchfiltersample.utils.ApiClient import com.example.recyclerviewwithsearchfiltersample.utils.Utility.hideProgressBar import com.example.recyclerviewwithsearchfiltersample.utils.Utility.isInternetAvailable import com.example.recyclerviewwithsearchfiltersample.utils.Utility.showProgressBar import kotlinx.android.synthetic.main.activity_main.* import retrofit2.Call import retrofit2.Callback import retrofit2.Response class MainActivity : AppCompatActivity() { private var listEmployees: MutableList<Employee> = mutableListOf<Employee>() private var employeesAdapter: EmployeesAdapter? = null private var mSearchView: SearchView? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) listEmployees = mutableListOf() recycler_main.layoutManager = LinearLayoutManager(this@MainActivity) employeesAdapter = EmployeesAdapter( this, listEmployees ) recycler_main.adapter = employeesAdapter if (isInternetAvailable()) { getEmployeesData() } } private fun getEmployeesData() { showProgressBar() ApiClient.apiService.getUsers().enqueue(object : Callback<MutableList<Employee>> { override fun onFailure(call: Call<MutableList<Employee>>, t: Throwable) { hideProgressBar() } override fun onResponse( call: Call<MutableList<Employee>>, response: Response<MutableList<Employee>> ) { hideProgressBar() val employeesResponse = response.body() listEmployees.clear() employeesResponse?.let { listEmployees.addAll(it) } employeesAdapter?.notifyDataSetChanged() } }) } override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_main, menu) val mSearchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager mSearchView = menu?.findItem(R.id.action_search)?.actionView as SearchView mSearchView?.setSearchableInfo(mSearchManager.getSearchableInfo(componentName)) mSearchView?.maxWidth = Int.MAX_VALUE mSearchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { employeesAdapter?.filter?.filter(query) return false } override fun onQueryTextChange(query: String?): Boolean { employeesAdapter?.filter?.filter(query) return false } }) return true } override fun onOptionsItemSelected(item: MenuItem?): Boolean { val id = item?.itemId return if (id == R.id.action_search) { true } else super.onOptionsItemSelected(item) } }


