Building Searchable and Filterable Lists in Android
Course: Android App Development
Topic: Building Searchable and Filterable Lists
Course Link: Android App Development Training
Demo Link: Register for a Free Course Demo
Website: JustAcademy
1. Introduction to Searchable and Filterable Lists
Searchable and filterable lists are essential features in modern Android applications. They allow users to find specific information from a large collection of items without manually scrolling through the entire list.
For example, an e-commerce application may contain thousands of products. Search functionality allows users to find a product by name, while filtering helps them display only products belonging to a particular category, price range, or rating.
In Android applications, these features are commonly implemented using RecyclerView, an adapter, a search input such as EditText or SearchView, and Kotlin collection operations.
Real-World Applications
- E-commerce apps: Search products and filter by category, price, and rating.
- Contact applications: Find contacts by name or phone number.
- Educational apps: Search courses and filter by skill level or subject.
- Food delivery apps: Filter restaurants by cuisine, rating, and delivery time.
- Music apps: Search songs and filter playlists by genre or artist.
- Employee directories: Filter employees by department, designation, or location.
2. Learning Objectives
After completing this topic, learners should be able to:
- Understand the difference between searching and filtering.
- Implement search functionality using Kotlin.
- Build searchable RecyclerView lists.
- Filter data using categories and multiple conditions.
- Combine search queries with filter selections.
- Handle empty search results and reset filters.
- Improve performance when working with large datasets.
3. Understanding Search and Filter Operations
3.1 What Is Searching?
Searching identifies items that match text entered by the user. The query is compared with one or more properties of each item, such as its name, description, or identifier.
For example, entering android into a course search box might display Android Development, Advanced Android, and Android UI Design.
3.2 What Is Filtering?
Filtering narrows a collection based on specific conditions. Instead of entering text, users may select a category, status, price range, rating, or other attribute.
For example, selecting the category Mobile Development displays only courses belonging to that category.
3.3 Difference Between Search and Filter
| Feature |
Searching |
Filtering |
| Purpose |
Find items matching a query. |
Narrow items using conditions. |
| Input |
Usually text. |
Category, checkbox, slider, dropdown, or other criteria. |
| Example |
Search for "Java". |
Show only beginner-level courses. |
| Implementation |
String matching. |
Boolean conditions and predicates. |
| Combined use |
Search text and filter conditions can be applied together to the same dataset. |
4. How Searchable Lists Work
A searchable list follows a simple workflow:
- The application loads its original dataset.
- The dataset is displayed using RecyclerView.
- The user enters text into the search field.
- The application compares the query with the relevant item properties.
- A filtered result list is created.
- The adapter updates the displayed items.
For responsive interfaces, the query should be processed efficiently, and expensive operations should not block the main thread.
5. Preparing the Data Model
Consider a course application where each course has an ID, title, category, and difficulty level. A Kotlin data class provides a convenient way to represent each item.
data class Course(
val id: Int,
val title: String,
val category: String,
val level: String
)
val courses = listOf(
Course(1, "Android Development", "Mobile", "Beginner"),
Course(2, "Advanced Kotlin", "Mobile", "Advanced"),
Course(3, "Python Programming", "Programming", "Beginner"),
Course(4, "Java Development", "Programming", "Intermediate"),
Course(5, "Flutter Development", "Mobile", "Intermediate")
)
The original list contains all available courses. Search and filter operations should work from this complete dataset rather than repeatedly filtering an already filtered list.
6. Implementing Basic Search in Kotlin
Kotlin provides the filter() collection function to return items that satisfy a condition. The contains() function can check whether a string includes the user's query.
fun searchCourses(
courses: List<Course>,
query: String
): List<Course> {
val normalizedQuery = query.trim()
if (normalizedQuery.isEmpty()) {
return courses
}
return courses.filter { course ->
course.title.contains(
normalizedQuery,
ignoreCase = true
)
}
}
Explanation:
trim() removes unnecessary spaces from the beginning and end of the query.
- An empty query returns the original dataset.
filter() creates a list containing matching courses.
ignoreCase = true makes the comparison case-insensitive.
For example, the query ANDROID will match Android Development.
7. Searching Across Multiple Fields
Users may expect to search by course title as well as category or difficulty level. This can be achieved by checking several properties in the same predicate.
fun searchCourses(
courses: List<Course>,
query: String
): List<Course> {
val text = query.trim()
if (text.isEmpty()) return courses
return courses.filter { course ->
course.title.contains(text, ignoreCase = true) ||
course.category.contains(text, ignoreCase = true) ||
course.level.contains(text, ignoreCase = true)
}
}
The logical OR operator (||) means an item is included if any one of the specified properties matches the query.
This approach is useful for searchable contact lists, product catalogs, employee directories, and course libraries.
8. Setting Up RecyclerView for Search Results
RecyclerView efficiently displays large collections by reusing item views as the user scrolls. Its adapter binds data to individual rows and reports changes to the displayed collection.
A typical implementation requires:
- A RecyclerView in the screen layout.
- A row layout for each item.
- A data model.
- An adapter that binds items to views.
- A search field.
- A function that updates the adapter's dataset.
8.1 Example Layout
The following simplified XML layout places a search field above a RecyclerView.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/searchInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Search courses"
android:inputType="text"
android:singleLine="true" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/courseRecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
This example uses EditText as the search input. An Android SearchView or a Material Design text field can also be used depending on the application's design.
9. Creating a RecyclerView Adapter
The adapter is responsible for displaying the current list of courses. The following example uses a basic RecyclerView adapter with a TextView in each row.
Assume that the row layout is named item_course.xml and contains a TextView with the ID courseTitle.
class CourseAdapter(
private var items: List<Course>
) : RecyclerView.Adapter<CourseAdapter.CourseViewHolder>() {
class CourseViewHolder(
val titleView: TextView
) : RecyclerView.ViewHolder(titleView)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): CourseViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_course, parent, false)
return CourseViewHolder(
view.findViewById(R.id.courseTitle)
)
}
override fun onBindViewHolder(
holder: CourseViewHolder,
position: Int
) {
holder.titleView.text = items[position].title
}
override fun getItemCount(): Int = items.size
fun updateItems(newItems: List<Course>) {
items = newItems
notifyDataSetChanged()
}
}
Important: This is a teaching example. For production lists, prefer ListAdapter with DiffUtil so that RecyclerView can calculate item-level changes instead of refreshing every row.
10. Connecting Search Input to RecyclerView
After creating the adapter, connect the search field to the list. Each time the query changes, calculate the matching courses and update the adapter.
private lateinit var courseAdapter: CourseAdapter
private lateinit var allCourses: List<Course>
private fun setupSearch() {
val searchInput = findViewById<EditText>(
R.id.searchInput
)
searchInput.addTextChangedListener(
object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) = Unit
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
val query = s?.toString().orEmpty()
val results = allCourses.filter { course ->
course.title.contains(
query.trim(),
ignoreCase = true
)
}
courseAdapter.updateItems(results)
}
override fun afterTextChanged(
s: Editable?
) = Unit
}
)
}
Initialize allCourses, the RecyclerView layout manager, and courseAdapter before calling setupSearch(). The example demonstrates the search interaction; a complete screen must also inflate the row layout and connect the adapter to the RecyclerView.
11. Using SearchView Instead of EditText
SearchView is a standard Android widget designed for entering search queries. It can be placed in a layout or used as an app-bar search action.
For a SearchView placed directly in a layout, a query listener can update the list.
searchView.setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(
query: String?
): Boolean {
filterCourses(query.orEmpty())
return true
}
override fun onQueryTextChange(
newText: String?
): Boolean {
filterCourses(newText.orEmpty())
return true
}
}
)
Here, filterCourses() is an application function that searches the original list and updates the adapter. Choose the SearchView class that matches your project: the platform widget or the AndroidX AppCompat widget.
12. Implementing Category Filters
Search and filtering are different operations. A category filter displays only items that satisfy a selected category.
fun filterByCategory(
courses: List<Course>,
selectedCategory: String
): List<Course> {
if (selectedCategory == "All") {
return courses
}
return courses.filter { course ->
course.category.equals(
selectedCategory,
ignoreCase = true
)
}
}
For example, selecting Mobile returns Android Development, Advanced Kotlin, and Flutter Development from the sample dataset.
A Spinner, dropdown menu, choice chips, or radio buttons can provide the category selection interface.
13. Combining Search and Filters
A useful list screen allows users to search by text while a category or level filter is active. Both conditions should be evaluated against the original dataset.
fun applySearchAndFilters(
courses: List<Course>,
query: String,
category: String,
level: String
): List<Course> {
val text = query.trim()
return courses.filter { course ->
val matchesText =
text.isEmpty() ||
course.title.contains(text, ignoreCase = true) ||
course.category.contains(text, ignoreCase = true)
val matchesCategory =
category == "All" ||
course.category.equals(category, ignoreCase = true)
val matchesLevel =
level == "All" ||
course.level.equals(level, ignoreCase = true)
matchesText && matchesCategory && matchesLevel
}
}
How it works:
- The text condition checks the query against the title and category.
- The category condition accepts every category when the selection is All.
- The level condition accepts every difficulty level when the selection is All.
- The AND operator (
&&) requires all active conditions to be satisfied.
For example, a user can search for Development, select Mobile, and select Beginner. Only items satisfying all three requirements will appear.
14. Filtering by Numeric Ranges
Product and property applications often need filters based on numeric values such as price, rating, duration, or distance.
data class Product(
val name: String,
val price: Double,
val rating: Double
)
fun filterProducts(
products: List<Product>,
minPrice: Double,
maxPrice: Double,
minRating: Double
): List<Product> {
return products.filter { product ->
product.price in minPrice..maxPrice &&
product.rating >= minRating
}
}
Numeric inputs should be validated before the function is called. For example, the minimum price should not exceed the maximum price, and the selected rating should fall within the application's permitted range.
15. Sorting Search Results
Sorting changes the order of matching items. It does not determine which items qualify for the results.
// Alphabetical order
val alphabetical = results.sortedBy { it.title }
// Reverse alphabetical order
val reverseAlphabetical = results.sortedByDescending {
it.title
}
For product lists, sorting can be based on price or rating. Apply sorting after the search and filter conditions when the intended behavior is to order the matching results.
16. Displaying Empty Search Results
When no item matches the user's query, the application should display a clear empty state instead of leaving the screen looking broken.
Example message: No courses found. Try another keyword or reset your filters.
val results = applySearchAndFilters(
courses = allCourses,
query = query,
category = selectedCategory,
level = selectedLevel
)
courseAdapter.updateItems(results)
emptyStateView.visibility =
if (results.isEmpty()) View.VISIBLE
else View.GONE
recyclerView.visibility =
if (results.isEmpty()) View.GONE
else View.VISIBLE
The empty-state view might include an illustration, a short explanation, and a button to clear the current filters.
17. Resetting Search and Filter Selections
A reset button restores the default list state. It should clear the search query, return category and level selections to All, and refresh the displayed items.
fun resetListFilters() {
searchInput.setText("")
selectedCategory = "All"
selectedLevel = "All"
updateCategorySelection("All")
updateLevelSelection("All")
refreshCourseList()
}
The names updateCategorySelection(), updateLevelSelection(), and refreshCourseList() represent application-specific functions that must be implemented. Keep the selected filter values in a single source of truth so the UI and displayed list remain synchronized.
18. Improving RecyclerView Update Efficiency with ListAdapter
For production applications, ListAdapter and DiffUtil provide an efficient way to update a list when the search query or filters change. DiffUtil compares the old and new datasets and identifies insertions, removals, moves, and content changes.
class CourseListAdapter :
ListAdapter<Course, CourseListAdapter.CourseViewHolder>(
object : DiffUtil.ItemCallback<Course>() {
override fun areItemsTheSame(
oldItem: Course,
newItem: Course
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: Course,
newItem: Course
): Boolean {
return oldItem == newItem
}
}
) {
// Implement onCreateViewHolder() and
// onBindViewHolder() for your row layout.
}
After filtering the original dataset, submit the results using submitList(results). The adapter can then calculate the differences and update the displayed collection.
19. Performance Considerations for Large Datasets
- Debounce rapid input: Avoid running expensive searches for every keystroke when the dataset is large.
- Use background processing: Perform expensive filtering or database operations off the main thread.
- Use ListAdapter and DiffUtil: Avoid unnecessarily rebinding every visible item.
- Use Room for local databases: Database queries can handle large collections more effectively than repeatedly loading every record into memory.
- Use server-side search where appropriate: Large remote datasets may be better searched through an API with pagination.
- Preserve the original data: Recompute results from the full dataset to avoid losing items after repeated searches.
- Normalize comparison rules: Trim input and consistently handle case and whitespace.
- Prevent stale results: When searches run asynchronously, cancel or ignore outdated requests where necessary.
20. Common Problems and Solutions
| Problem |
Possible Cause |
Solution |
| Search returns no results unexpectedly. |
Incorrect field, whitespace, or comparison logic. |
Trim the query and check the correct item properties. |
| Search works only once. |
The listener is not registered correctly or the list state is overwritten. |
Register the listener once and retain the original dataset. |
| Items disappear after clearing search. |
The code filters an already filtered list. |
Run each search against the original full collection. |
| Category filter ignores the search query. |
Search and filter functions operate independently. |
Combine their predicates in one result calculation. |
| RecyclerView shows outdated items. |
The adapter is not receiving updated results. |
Update the dataset or call ListAdapter.submitList(). |
| Scrolling becomes slow. |
Heavy work on the main thread or expensive row binding. |
Optimize filtering, use background processing, and simplify row binding. |
| Empty screen appears when nothing matches. |
No empty-state handling is implemented. |
Show an appropriate message and provide a reset action. |
21. Testing Search and Filter Functionality
Test the list with different types of queries and filter combinations before releasing the application.
- Search for a complete item title.
- Search using only part of a title.
- Test uppercase and lowercase queries.
- Enter leading and trailing spaces.
- Clear the query and confirm that every original item returns.
- Select each category and confirm that unrelated items are excluded.
- Combine category, level, and text filters.
- Test a query that produces zero results.
- Reset all filters and verify the default list.
- Test performance with a large dataset.
22. Practical Exercise
Project: Searchable Course Directory
Create an Android screen that displays a list of available training courses. The screen should allow users to search by course title, filter by category, and filter by difficulty level.
Requirements:
- Create a Course data class containing ID, title, category, and level.
- Display courses using RecyclerView.
- Add a search field that updates results as the user types.
- Add category and difficulty-level selectors.
- Allow search and both filters to work together.
- Display an empty-state message when no courses match.
- Add a reset button that restores the complete list.
- Use ListAdapter and DiffUtil for efficient updates.
Expected result: Users should be able to quickly find relevant courses without scrolling through the entire dataset.
23. Interview Questions and Answers
Q1. What is the difference between searching and filtering?
Answer: Searching typically matches a text query against item properties, whereas filtering selects items according to conditions such as category, rating, status, or price.
Q2. Why is RecyclerView useful for searchable lists?
Answer: RecyclerView reuses item views and supports efficient display of large collections. Its adapter can be updated whenever the search results change.
Q3. What does Kotlin's filter() function do?
Answer: It returns a new list containing only the elements that satisfy the supplied predicate. The original collection remains unchanged.
Q4. How can search be made case-insensitive?
Answer: Use functions such as contains(query, ignoreCase = true) when comparing strings.
Q5. Why should the original dataset be preserved?
Answer: It allows the application to recompute results correctly when the query changes or filters are cleared. Filtering an already filtered list can permanently exclude items from subsequent results.
Q6. How can search and category filters work together?
Answer: Evaluate the text-matching and category conditions in the same filtering operation. Use a logical AND when an item must satisfy both conditions.
Q7. What is the purpose of DiffUtil?
Answer: DiffUtil calculates differences between two lists so that the UI can update only the affected items rather than refreshing the entire collection unnecessarily.
Q8. How should an application handle empty results?
Answer: Display a helpful empty-state message and offer an action to clear the search query or reset the active filters.
Q9. How can large search operations be optimized?
Answer: Use debouncing for rapid input, background processing for expensive operations, database queries for large local datasets, and server-side search with pagination for large remote datasets.
Q10. What is the difference between sorting and filtering?
Answer: Filtering determines which items appear, while sorting determines the order in which those items appear.
24. Quick Revision
- Search finds items matching a query.
- Filtering narrows items using defined conditions.
- RecyclerView displays the results.
- An adapter binds data to individual item views.
- Kotlin's filter() function creates a matching collection.
- Search comparisons can be case-insensitive.
- Multiple filters can be combined with logical operators.
- The original dataset should be preserved.
- ListAdapter and DiffUtil support efficient UI updates.
- Empty states and reset controls improve usability.
25. Best Practices
- Keep search and filter logic separate from view-binding code when possible.
- Use a single, predictable source of truth for the original dataset and selected filter values.
- Validate numeric ranges and other user inputs.
- Keep the interface responsive while filtering large collections.
- Use meaningful empty-state messages.
- Test combinations of filters, not just each filter independently.
- Prefer database queries for large persistent datasets.
- Use stable item IDs when implementing DiffUtil.
Conclusion
Building searchable and filterable lists is an important Android development skill. By combining Kotlin collection operations, RecyclerView, appropriate search controls, category selectors, and efficient adapter updates, developers can build interfaces that help users locate relevant information quickly.
Start with a small in-memory list, implement text search, add category and level filters, and then improve performance using ListAdapter, DiffUtil, and database queries where appropriate.
Explore Android App Development Training
Learn more about Android application development and related practical topics through JustAcademy.
Explore the Android App Development Course
Register for a Course Demo
Visit the JustAcademy Website