Skip to main content
This guide walks you through the steps needed to create a full InstantSearch Android experience from scratch. This guide walks you through the steps needed to create a full InstantSearch Android experience from scratch. It includes:
  • A search box for users to type queries
  • A list to display search results
  • A list to results
  • Statistics about the search

Before you begin

To use InstantSearch, you need an Algolia account. Sign up for a new account or use the following credentials (which include a pre-loaded dataset of products appropriate for this guide):
  • : latency
  • Search : 1f6fd3a6fb973cb08419fe7d288fa4db
  • name: instant_search

Create a new project and add InstantSearch Android

In Android Studio, create a new Project:
  • On the Target screen, select Phone and Tablet
  • On the Add an Activity screen, select Empty Activity
In your app’s build.gradle, add the following dependency:
build.gradle
implementation 'com.algolia:instantsearch-android:4.+'
implementation 'com.algolia:instantsearch-android-paging3:4.+'
This guide uses InstantSearch Android with Android Architecture Components, so you also need to add the following dependencies:
build.gradle
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.+'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.+'
To perform network operations in your app, AndroidManifest.xml must include the following permissions:
XML
<uses-permission android:name="android.permission.INTERNET" />
Setup kotlinx.serialization by adding the serialization plugin to your build.gradle:
build.gradle
plugins {
  // ...
  id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.0'
}

Implementation

Architecture overview

  • MainActivity: This activity controls the displayed fragment.
  • MyViewModel: A ViewModel from Android Architecture Components. The business logic lives here.
  • ProductFragment: This fragment displays a list of search results in a RecyclerView, a SearchView input, and a Stats indicator.
  • FacetFragment: This fragment displays a list of facets to filter your search results.

Initialize a searcher

The central part of your search experience is the Searcher. The Searcher performs a and obtains search results. Most InstantSearch components connected to the Searcher. In this guide, you’ll only target one index, so instantiate a HitsSearcher with the proper credentials. Go to MyViewModel.kt file and add the following code:
Kotlin
class MyViewModel : ViewModel() {

    val searcher = HitsSearcher(
        applicationID = "latency",
        apiKey = "1f6fd3a6fb973cb08419fe7d288fa4db",
        indexName = "instant_search"
    )

    override fun onCleared() {
        super.onCleared()
        searcher.cancel()
    }
}
A ViewModel is a good place to put your data sources. This way, the data persists during orientation changes and you can share it across multiple fragments.

Display results: Hits

Suppose you want to display search results in a RecyclerView. To simultaneously provide a good user experience and display thousands of products, you can implement an infinite scrolling mechanism using the Paging Library from Android Architecture Component. Screenshot of a mobile app showing search results under 'Hits,' listing items like 'Webroot SecureAnywhere' and 'Apple® - 3.3' Lightning-to-USB 2.0 Cable' with a 'FILTERS' button. To display your results:
1

Create the data source

Create a LiveData object, which holds a PagedList of Product. Create the Product data class which contains a single name field.
Kotlin
@Serializable
data class Product(
    val name: String
)
2

Define the item layout

Create the product_item.xml file.
XML
<com.google.android.material.card.MaterialCardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="?attr/listPreferredItemHeightSmall"
    android:layout_marginBottom="0.5dp"
    app:cardCornerRadius="0dp"
    tools:layout_height="50dp">

    <TextView
        android:id="@+id/productName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:ellipsize="end"
        android:maxLines="1"
        android:textAppearance="?attr/textAppearanceBody1"
        android:textSize="16sp"
        tools:text="@tools:sample/lorem/random" />

</com.google.android.material.card.MaterialCardView>
3

Implement a ViewHolder

Create the ProductViewHolder to bind a Product item to a RecyclerView.ViewHolder.
Kotlin
class ProductViewHolder(view: View) : RecyclerView.ViewHolder(view) {

    private val itemName = view.findViewById<TextView>(R.id.itemName)

    fun bind(product: Product) {
        itemName.text = product.name
    }
}
4

Create an adapter

Create a ProductAdapter by extending PagingDataAdapter. The ProductAdapter binds products to the ViewHolder.
Kotlin
class ProductAdapter : PagingDataAdapter<Product, ProductViewHolder>(ProductDiffUtil) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
        return ProductViewHolder(parent.inflate(R.layout.list_item_small))
    }

    override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
        getItem(position)?.let { holder.bind(it) }
    }

    object ProductDiffUtil : DiffUtil.ItemCallback<Product>() {
        override fun areItemsTheSame(oldItem: Product, newItem: Product) = oldItem.objectID == newItem.objectID
        override fun areContentsTheSame(oldItem: Product, newItem: Product) = oldItem == newItem
    }
}
5

Connect the searcher and paginator

In your ViewModel use a Paginator with your searcher:
Kotlin
class MyViewModel : ViewModel() {

    // Searcher initialization
    // ...
    val paginator = Paginator(
        searcher = searcher,
        pagingConfig = PagingConfig(pageSize = 50, enablePlaceholders = false),
        transformer = { hit -> hit.deserialize(Product.serializer()) }
    )

    override fun onCleared() {
        super.onCleared()
        searcher.cancel()
    }
}
6

Create the fragment layoutr

Now that your ViewModel has some data, you can create a simple product_fragment.xml with a Toolbar and a RecyclerView to display the products:
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.Toolbar
        android:id="@+id/toolbar"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_width="0dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_height="?attr/actionBarSize"/>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/productList"
        app:layout_constraintTop_toBottomOf="@id/toolbar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_width="0dp"
        android:layout_height="0dp"
        tools:listitem="@layout/product_item"/>

</androidx.constraintlayout.widget.ConstraintLayout>
7

Observe and display results

In the ProductFragment, get a reference of MyViewModel using activityViewModels(). Then, observe the LiveData to update the ProductAdapter on every new page of products. Finally, configure the RecyclerView by setting its adapter and LayoutManager.
Kotlin
class ProductFragment : Fragment(R.layout.fragment_product) {

    private val viewModel: MyViewModel by activityViewModels()
    private val connection = ConnectionHandler()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val adapterProduct = ProductAdapter()
        viewModel.paginator.liveData.observe(viewLifecycleOwner) { pagingData ->
            adapterProduct.submitData(lifecycle, pagingData)
        }
        view.findViewById<RecyclerView>(R.id.productList).let {
            it.itemAnimator = null
            it.adapter = adapterProduct
            it.layoutManager = LinearLayoutManager(requireContext())
            it.autoScrollToStart(adapterProduct)
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        connection.clear()
    }
}
To display the ProductFragment, update activity_main.xml to have a container for the fragments:
XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
Update MainActivity to display ProductFragment:
Kotlin
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        showProductFragment()
    }

    fun showProductFragment() {
        supportFragmentManager.commit {
            replace<ProductFragment>(R.id.container)
        }
    }
}
You have now learned how to display search results in an infinite scrolling RecyclerView.
To search your data, users need an input field. Any change in this field should trigger a new request, and then update the search results. To achieve this, use a SearchBoxConnector. This takes a Searcher and connected to Pagiantor using connectPaginator.
Kotlin
class MyViewModel : ViewModel() {

    // Searcher initialization
    // Hits initialization
    // ...

    val searchBox = SearchBoxConnector(searcher)
    val connection = ConnectionHandler(searchBox)

    init {
        connection += searchBox.connectPaginator(paginator)
    }

    override fun onCleared() {
        super.onCleared()
        searcher.cancel()
        connection.clear()
    }
}
Most InstantSearch components should be connected and disconnected in accordance to the Android Lifecycle to avoid memory leaks. A ConnectionHandler handles a set of Connection s for you: Each += call with a component implementing the Connection interface will connect it and make it active. Whenever you want to free resources or deactivate a component, call the disconnect method.
You can now add a SearchView in your Toolbar:
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.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="0dp"
        android:layout_height="?attr/actionBarSize"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipChildren="false"
            android:clipToPadding="false">

            <androidx.appcompat.widget.SearchView
                android:id="@+id/searchView"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:focusable="false"
                app:iconifiedByDefault="false"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.appcompat.widget.Toolbar>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/productList"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/toolbar"
        tools:listitem="@layout/product_item" />

</androidx.constraintlayout.widget.ConstraintLayout>
Connect the SearchBoxViewAppCompat to the SearchBoxConnectorPagedList stored in MyViewModel using a new ConnectionHandler that conforms to the ProductFragment lifecycle:
Kotlin
class ProductFragment : Fragment(R.layout.fragment_product) {

    private val viewModel: MyViewModel by activityViewModels()
    private val connection = ConnectionHandler()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Hits
        // ...

        val searchBoxView = SearchBoxViewAppCompat(searchView)
        connection += viewModel.searchBox.connectView(searchBoxView)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        connection.clear()
    }
}
You can now build and run your app, and see a basic search experience. The results change with each key stroke.

Displaying metadata: Stats

It’s a good practice to show the number of hits that were returned for a search. You can use the Stats components to do this with minimal code. Add a StatsConnector to your MyViewModel, and connect it with a ConnectionHandler.
Kotlin
class MyViewModel : ViewModel() {

    // Searcher initialization
    // Hits initialization
    // SearchBox initialization
    // ...

    val stats = StatsConnector(searcher)

    val connection = ConnectionHandler(searchBox, stats)

    override fun onCleared() {
        super.onCleared()
        searcher.cancel()
        connection.clear()
    }
}
Add a TextView to your product_fragment.xml file to display the stats.
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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="0dp"
        android:layout_height="?attr/actionBarSize"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipChildren="false"
            android:clipToPadding="false">

            <androidx.appcompat.widget.SearchView
                android:id="@+id/searchView"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:focusable="false"
                app:iconifiedByDefault="false"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.appcompat.widget.Toolbar>

    <TextView
        android:id="@+id/stats"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/productList"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/stats"
        tools:listitem="@layout/product_item" />

</androidx.constraintlayout.widget.ConstraintLayout>
Next, connect the StatsConnector to a StatsTextView in your ProductFragment.
Kotlin
class ProductFragment : Fragment(R.layout.fragment_product) {

    private val viewModel: MyViewModel by activityViewModels()
    private val connection = ConnectionHandler()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Hits
        // SearchBox
        // ...

        val statsView = StatsTextView(view.findViewById(R.id.stats))
        connection += viewModel.stats.connectView(statsView, DefaultStatsPresenter())
    }

    override fun onDestroyView() {
        super.onDestroyView()
        connection.clear()
    }
}
You can now build and run your app and see your new search experience in action.

Filter your data: FacetList

Screenshot of a mobile interface showing a list of categories with counts, where 'Cell Phone Cases and Clips' is selected with a checkmark. Filtering search results helps your users find what they want. You can create a FacetList to filter products by category. Create a drawable resource ic_check.xml. This resource displays checked filters.
XML
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>
Create a facet_item.xml file. This is the layout for a RecyclerView.ViewHolder.
XML
<com.google.android.material.card.MaterialCardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="?attr/listPreferredItemHeightSmall"
    android:layout_marginBottom="0.5dp"
    app:cardCornerRadius="0dp"
    tools:layout_height="50dp">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="12dp"
            android:src="@drawable/ic_check"
            android:tint="?attr/colorPrimary"
            android:visibility="invisible"
            app:layout_constrainedHeight="true"
            app:layout_constrainedWidth="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:visibility="visible" />

        <TextView
            android:id="@+id/facetCount"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:ellipsize="end"
            android:gravity="end"
            android:maxLines="1"
            android:textAppearance="?attr/textAppearanceBody2"
            android:textColor="#818794"
            android:textSize="16sp"
            android:visibility="gone"
            app:layout_constrainedHeight="true"
            app:layout_constrainedWidth="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@id/icon"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="@tools:sample/lorem"
            tools:visibility="visible" />

        <TextView
            android:id="@+id/facetName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp"
            android:ellipsize="end"
            android:maxLines="1"
            android:textAppearance="?attr/textAppearanceBody1"
            android:textSize="16sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@id/facetCount"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="@tools:sample/lorem/random" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</com.google.android.material.card.MaterialCardView>
Implement the FacetListViewHolder and its Factory, so that later on it works with a FacetListAdapter.
Kotlin
import com.algolia.client.model.search.FacetHits

class MyFacetListViewHolder(view: View) : FacetListViewHolder(view) {

    override fun bind(facet: FacetHits, selected: Boolean, onClickListener: View.OnClickListener) {
        view.setOnClickListener(onClickListener)
        val facetCount = view.findViewById<TextView>(R.id.facetCount)
        facetCount.text = facet.count.toString()
        facetCount.visibility = View.VISIBLE
        view.findViewById<ImageView>(R.id.icon).visibility = if (selected) View.VISIBLE else View.INVISIBLE
        view.findViewById<TextView>(R.id.facetName).text = facet.value
    }

    object Factory : FacetListViewHolder.Factory {

        override fun createViewHolder(parent: ViewGroup): FacetListViewHolder {
            return MyFacetListViewHolder(parent.inflate(R.layout.list_facet_selectable))
        }
    }
}
You can use a new component to handle the filtering logic: the FilterState. Pass the FilterState to your FacetListConnector. The FacetListConnector needs an attribute: you should use "categories". Inject MyFacetListViewHolder.Factory into your FacetListAdapter. The FacetListAdapter is an out of the box RecyclerView.Adapter for a FacetList. Connect the different parts together:
  • The Searcher connects itself to the FilterState, and applies its filters with every search.
  • The FilterState connects to your products Paginator to invalidate search results when new filter are applied.
  • The facetList connects to its adapter.
Kotlin
class MyViewModel : ViewModel() {

    // Client, Searcher...
    // Products
    // Stats
    // ...

    val filterState = FilterState()
    val facetList = FacetListConnector(
        searcher = searcher,
        filterState = filterState,
        attribute = "categories",
        selectionMode = SelectionMode.Single
    )
    val facetPresenter = DefaultFacetListPresenter(
        sortBy = listOf(FacetSortCriterion.CountDescending, FacetSortCriterion.IsRefined),
        limit = 100
    )
    val connection = ConnectionHandler(searchBox, stats, facetList)

    init {
        // SearchBox
        // ..

        connection += searcher.connectFilterState(filterState)
        connection += filterState.connectPaginator(paginator)
    }

    override fun onCleared() {
        super.onCleared()
        searcher.cancel()
        connection.clear()
    }
}
To display your facets, create a facet_fragment.xml layout with a RecyclerView.
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"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_width="0dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_height="?attr/actionBarSize"/>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/facetList"
        android:background="#FFFFFF"
        app:layout_constraintTop_toBottomOf="@id/toolbar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_width="0dp"
        android:layout_height="0dp"
        tools:listitem="@layout/facet_item"/>

</androidx.constraintlayout.widget.ConstraintLayout>
Create a FacetFragment and configure your RecyclerView with its adapter and LayoutManager.
Kotlin
class FacetFragment : Fragment(R.layout.fragment_facet) {

    private val viewModel: MyViewModel by activityViewModels()
    private val connection = ConnectionHandler()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val adapterFacet = FacetListAdapter(MyFacetListViewHolder.Factory)
        val facetList = view.findViewById<RecyclerView>(R.id.facetList)
        connection += viewModel.facetList.connectView(adapterFacet, viewModel.facetPresenter)

        facetList.let {
            it.adapter = adapterFacet
            it.layoutManager = LinearLayoutManager(requireContext())
            it.autoScrollToStart(adapterFacet)
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        connection.clear()
    }
}
Add the code to switch to FacetFragment in MainActivity with “navigation up” support.
Kotlin
class MainActivity : AppCompatActivity() {

    val viewModel: MyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_getting_started)
        showProductFragment()
    }

    private fun showFacetFragment() {
        supportFragmentManager.commit {
            add<FacetFragment>(R.id.container)
            addToBackStack("facet")
        }
    }

    private fun showProductFragment() {
        supportFragmentManager.commit {
            replace<ProductFragment>(R.id.container)
        }
    }

    override fun onSupportNavigateUp(): Boolean {
        if (supportFragmentManager.popBackStackImmediate()) return true
        return super.onSupportNavigateUp()
    }
}
Add a filters button in product_fragment.xml
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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="0dp"
        android:layout_height="?attr/actionBarSize"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipChildren="false"
            android:clipToPadding="false">

            <com.google.android.material.button.MaterialButton
                android:id="@+id/filters"
                style="@style/Widget.MaterialComponents.Button.TextButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="6dp"
                android:layout_marginEnd="12dp"
                android:text="Filters"
                android:textAppearance="@style/TextAppearance.MaterialComponents.Button"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@id/searchView"
                app:layout_constraintTop_toTopOf="parent" />

            <androidx.appcompat.widget.SearchView
                android:id="@+id/searchView"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:focusable="false"
                app:iconifiedByDefault="false"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@id/filters"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.appcompat.widget.Toolbar>

    <TextView
        android:id="@+id/stats"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/productList"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/stats"
        tools:listitem="@layout/product_item" />

</androidx.constraintlayout.widget.ConstraintLayout>
Add displayFilters and navigateToFilters() to MyViewModel to trigger filters display:
Kotlin
class MyViewModel : ViewModel() {

    // Client, Searcher...
    // Products
    // Stats
    // FilterState
    // ...

    private val _displayFilters = MutableLiveData<Unit>()
    val displayFilters: LiveData<Unit> get() = _displayFilters

    fun navigateToFilters() {
        _displayFilters.value = Unit
    }

    override fun onCleared() {
        super.onCleared()
        searcher.cancel()
        connection.clear()
    }
}
In ProductFragment, add a listener to the filters button to switch to FacetFragment:
Kotlin
class ProductFragment : Fragment() {

    private val connection = ConnectionHandler()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.product_fragment, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val viewModel = ViewModelProvider(requireActivity())[MyViewModel::class.java]

        // Hits
        // SearchBox
        // Stats
        // ...

        view.findViewById<Button>(R.id.filters).setOnClickListener {
            viewModel.navigateToFilters()
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        connection.clear()
    }
}
Add navigation setup to display FacetFragment:
Kotlin
class MainActivity : AppCompatActivity() {

    val viewModel: MyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_getting_started)
        showProductFragment()
        setupNavigation()
    }

    private fun showProductFragment() {
        supportFragmentManager.commit {
            replace<ProductFragment>(R.id.container)
        }
    }

    private fun setupNavigation() {
        viewModel.displayFilters.observe(this) {
            showFacetFragment()
        }
    }

    private fun showFacetFragment() {
        supportFragmentManager.commit {
            add<FacetFragment>(R.id.container)
            addToBackStack("facet")
        }
    }

    override fun onSupportNavigateUp(): Boolean {
        if (supportFragmentManager.popBackStackImmediate()) return true
        return super.onSupportNavigateUp()
    }
}
You can now build and run your app to see your advanced search experience. This experience helps your users to filter search results and find what they’re looking for.

Improving the user experience: Highlighting

Highlighting enhances the user experience by putting emphasis on the parts of the result that match the . It’s a visual indication of why a result is relevant to the query. You can add highlighting by implementing the Highlightable interface on Product. Define a highlightedName field to retrieve the highlighted value for the name attribute.
Kotlin
@Serializable
data class Product(
    val name: String,
    override val _highlightResult: JsonObject?
) : Highlightable {

    public val highlightedName: HighlightedString?
        get() = getHighlight("name")
}
Use the .toSpannedString() extension function to convert an HighlightedString into a SpannedString that can be assigned to a TextView to display the highlighted names.
Kotlin
class ProductViewHolder(view: View) : RecyclerView.ViewHolder(view) {

    private val itemName = view.findViewById<TextView>(R.id.itemName)

    fun bind(product: Product) {
        itemName.text = product.highlightedName?.toSpannedString() ?: product.name
    }
}

Conclusion

You now have a fully working search experience: your users can search for products, refine their results, and understand the number of returned and why they’re relevant to the query. Screenshot of a mobile search with 'Hello' entered, showing results for 'Hello Kitty' products and a keyboard. Find the full source code in GitHub.

Going further

This is only an introduction to what you can do with InstantSearch Android: check out the widget showcase to see more components.
Last modified on February 18, 2026