> ## Documentation Index
> Fetch the complete documentation index at: https://algolia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a Query Suggestions UI

> Build a user interface to show Query Suggestions in your InstantSearch Android app.

export const FlavorSwitcher = ({current, baseHref = "", options = [], label = "InstantSearch framework"}) => {
  if (options.length === 0) {
    return <div className="not-prose" role="alert" style={{
      margin: "0.25rem 0 1.5rem",
      padding: "0.75rem",
      border: "1px solid #f59e0b",
      borderRadius: "0.625rem",
      color: "inherit",
      fontSize: "0.875rem"
    }}>
        FlavorSwitcher requires at least one option.
      </div>;
  }
  const selected = options.find(option => option.value === current) ?? options[0];
  return <div className="not-prose mint-flavor-switcher">
      <style>{`
        .mint-flavor-switcher {
          --mfs-bg: #ffffff;
          --mfs-bg-hover: #f4f4f5;
          --mfs-bg-current: #eef2ff;
          --mfs-border: #d4d4d8;
          --mfs-fg: #18181b;
          --mfs-muted: #71717a;
          --mfs-accent: #4f46e5;
          position: relative;
          width: min(100%, 19rem);
          margin: 0.25rem 0 1.5rem;
          color: var(--mfs-fg);
          font-size: 0.875rem;
          line-height: 1.25rem;
        }

        .dark .mint-flavor-switcher {
          --mfs-bg: #18181b;
          --mfs-bg-hover: #27272a;
          --mfs-bg-current: #272747;
          --mfs-border: #3f3f46;
          --mfs-fg: #fafafa;
          --mfs-muted: #a1a1aa;
          --mfs-accent: #a5b4fc;
        }

        .mint-flavor-switcher details {
          position: relative;
        }

        .mint-flavor-switcher summary {
          display: flex;
          min-height: 2.75rem;
          box-sizing: border-box;
          align-items: center;
          justify-content: space-between;
          gap: 0.75rem;
          padding: 0.625rem 0.75rem;
          border: 1px solid var(--mfs-border);
          border-radius: 0.625rem;
          background: var(--mfs-bg);
          color: var(--mfs-fg);
          cursor: pointer;
          font-weight: 600;
          list-style: none;
          transition: border-color 150ms ease, box-shadow 150ms ease;
        }

        .mint-flavor-switcher summary::-webkit-details-marker {
          display: none;
        }

        .mint-flavor-switcher summary:hover {
          border-color: var(--mfs-accent);
        }

        .mint-flavor-switcher summary:focus-visible {
          outline: 2px solid var(--mfs-accent);
          outline-offset: 2px;
        }

        .mint-flavor-switcher__label {
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }

        .mint-flavor-switcher__chevron {
          flex: none;
          transition: transform 150ms ease;
        }

        .mint-flavor-switcher details[open] .mint-flavor-switcher__chevron {
          transform: rotate(180deg);
        }

        .mint-flavor-switcher__menu {
          position: absolute;
          z-index: 50;
          top: calc(100% + 0.375rem);
          left: 0;
          width: 100%;
          box-sizing: border-box;
          margin: 0;
          padding: 0.375rem;
          border: 1px solid var(--mfs-border);
          border-radius: 0.625rem;
          background: var(--mfs-bg);
          box-shadow: 0 12px 30px rgb(0 0 0 / 16%);
          list-style: none;
        }

        .mint-flavor-switcher__menu li {
          margin: 0;
          padding: 0;
        }

        .mint-flavor-switcher__option {
          display: grid;
          gap: 0.125rem;
          padding: 0.625rem 0.75rem;
          border-radius: 0.4rem;
          color: var(--mfs-fg);
          text-decoration: none;
        }

        .mint-flavor-switcher__option:hover {
          background: var(--mfs-bg-hover);
        }

        .mint-flavor-switcher__option:focus-visible {
          outline: 2px solid var(--mfs-accent);
          outline-offset: -2px;
        }

        .mint-flavor-switcher__option[aria-current="page"] {
          background: var(--mfs-bg-current);
          color: var(--mfs-accent);
        }

        .mint-flavor-switcher__name {
          font-weight: 600;
        }

        .mint-flavor-switcher__description {
          color: var(--mfs-muted);
          font-size: 0.8125rem;
        }

        @media (prefers-reduced-motion: reduce) {
          .mint-flavor-switcher summary,
          .mint-flavor-switcher__chevron {
            transition: none;
          }
        }
      `}</style>

      <details>
        <summary aria-label={`${label}: ${selected.label}`}>
          <span className="mint-flavor-switcher__label">{selected.label}</span>
          <svg className="mint-flavor-switcher__chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="m6 9 6 6 6-6" />
          </svg>
        </summary>

        <ul className="mint-flavor-switcher__menu" aria-label={label}>
          {options.map(option => {
    const isCurrent = option.value === selected.value;
    const href = option.href ?? `${baseHref.replace(/\/$/, "")}/${encodeURIComponent(option.value)}`;
    return <li key={option.value}>
                <a className="mint-flavor-switcher__option" href={href} aria-current={isCurrent ? "page" : undefined}>
                  <span className="mint-flavor-switcher__name">
                    {option.label}
                  </span>
                  {option.description ? <span className="mint-flavor-switcher__description">
                      {option.description}
                    </span> : null}
                </a>
              </li>;
  })}
        </ul>
      </details>
    </div>;
};

<div className="mint-flavor-switcher-slot not-prose">
  <FlavorSwitcher
    current="android"
    baseHref="/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/tutorials/building-query-suggestions-ui"
    options={[
{ value: "js", label: "JavaScript", description: "InstantSearch.js" },
{ value: "react", label: "React", description: "React InstantSearch" },
{ value: "vue", label: "Vue", description: "Vue InstantSearch" },
{ value: "ios", label: "iOS", description: "InstantSearch iOS" },
{ value: "android", label: "Android", description: "InstantSearch Android" },
]}
  />
</div>

When your user interacts with a search box,
you can help them discover what they could search for by providing **Query suggestions**.

Query suggestions are a specific kind of multi-index interface:

* The main search interface will use a regular index.
* As users type a phrase, suggestions from your [Query Suggestions index](/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/android) are displayed.

## Usage

To display the suggestions:

* Create a Query Suggestions index from your main index.
* Implement a Multi-Index search experience using both indices.
* When clicking on a suggestion, set the query to the chosen suggestion.

## Before you begin

To use InstantSearch Android, you need an Algolia account.
Either [create a new account](https://dashboard.algolia.com/users/sign_up) or use the following credentials:

* Application ID: `latency`
* Search API key: `afc3dd66dd1293e2e2736a5a51b05c0a`
* Results index name: `STAGING_native_ecom_demo_products`
* Suggestions index name: `STAGING_native_ecom_demo_products_query_suggestions`

These credentials give you access to pre-existing datasets of products and Query Suggestions appropriate for this guide.

## Project structure

Algolia's query suggestions uses:

* `QuerySuggestionGuide`: main activity presenting the search experience,
* `SuggestionFragment`: fragment presenting the Query Suggestions,
* `ProductFragment`: fragment presenting the search results,
* `QuerySuggestionViewModel`: view model holding connectors and search business logic.

The initial screen shows the search box and results for an empty query:

<img src="https://mintcdn.com/algolia/BaxS3Doxn613Z9Q0/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/tutorials/building-query-suggestions-ui/initial.png?fit=max&auto=format&n=BaxS3Doxn613Z9Q0&q=85&s=89725227cf6b67b8578e0e78f9a8cad7" alt="Initial state of the search interface on Android showing the search box and the results for an empty search query." width="450" height="963" data-path="doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/tutorials/building-query-suggestions-ui/initial.png" />

When users tap the search box, a list of query suggestions are shown (the most popular for an empty query):

<img src="https://mintcdn.com/algolia/BaxS3Doxn613Z9Q0/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/tutorials/building-query-suggestions-ui/suggestions.png?fit=max&auto=format&n=BaxS3Doxn613Z9Q0&q=85&s=96b959b609da6bce733f68ca6cbfb976" alt="List of popular query suggestions in the search interface on Android." width="450" height="963" data-path="doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/tutorials/building-query-suggestions-ui/suggestions.png" />

On each keystroke, the list of suggestions is updated:

<img src="https://mintcdn.com/algolia/BaxS3Doxn613Z9Q0/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/tutorials/building-query-suggestions-ui/suggestions-search.png?fit=max&auto=format&n=BaxS3Doxn613Z9Q0&q=85&s=f2f5af46a202cba4dacc24e1d5492c2a" alt="The list of Query Suggestions updates on every keystroke." width="450" height="963" data-path="doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/tutorials/building-query-suggestions-ui/suggestions-search.png" />

When users select a suggestion from the list, it replaces the query in the search box, and the suggestions fragment disappears.
The products fragment presents search results for the selected query suggestion:

<img src="https://mintcdn.com/algolia/BaxS3Doxn613Z9Q0/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/tutorials/building-query-suggestions-ui/results.png?fit=max&auto=format&n=BaxS3Doxn613Z9Q0&q=85&s=dec7f9666af4baa6dec501db6dc48212" alt="After accepting a query suggestion the results of the search are shown." width="450" height="963" data-path="doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/tutorials/building-query-suggestions-ui/results.png" />

Algolia doesn't provide a ready-to-use views, but you can create them with the tools in the [InstantSearch Android](https://github.com/algolia/instantsearch-android) library.

## Business logic

Create `QuerySuggestionViewModel` view to setup the search components and create the necessary connections between them,
establishing the business logic:

```kotlin Kotlin icon=code theme={"system"}
class QuerySuggestionViewModel : ViewModel() {

    val multiSearcher = MultiSearcher(
        applicationID = "latency",
        apiKey = "927c3fe76d4b52c5a2912973f35a3077"
    )
    val productSearcher = multiSearcher.addHitsSearcher(indexName = "STAGING_native_ecom_demo_products")
    val suggestionSearcher = multiSearcher.addHitsSearcher(indexName = "STAGING_native_ecom_demo_products_query_suggestions")
    val searchBox = SearchBoxConnector(multiSearcher)
    val suggestions = MutableLiveData<Suggestion>()

    override fun onCleared() {
        multiSearcher.cancel()
        searchBox.disconnect()
    }
}
```

## Products view

Create `Product` data class corresponding the index hits:

```kotlin Kotlin icon=code theme={"system"}
import com.algolia.instantsearch.core.Indexable

@Serializable
data class Product(
    val name: String,
    @SerialName("image_urls") val images: List<String>,
    val price: Price,
    val description: String,
    override val objectID: String,
    override val _highlightResult: JsonObject?
) : Indexable, Highlightable {

    val highlightedName: HighlightedString?
        get() = getHighlight("name")
}

@Serializable
data class Price(
    val currency: String,
    val value: String,
)
```

Create `ProductAdapter` to display search results:

```kotlin Kotlin icon=code theme={"system"}
class ProductAdapter : ListAdapter<Product, ProductViewHolder>(ProductDiffUtil), HitsView<Product> {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        ProductViewHolder(parent.inflate(R.layout.list_item_large))

    override fun onBindViewHolder(holder: ProductViewHolder, position: Int) =
        holder.bind(getItem(position))

    override fun setHits(hits: List<Product>) = submitList(hits)

    class ProductViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {

        fun bind(item: Product) {
            view.findViewById<TextView>(R.id.itemTitle).text =
                item.highlightedName?.toSpannedString() ?: item.name
            view.findViewById<TextView>(R.id.itemSubtitle).text = item.price.value
            Glide
                .with(view.context)
                .load(item.images.first())
                .into(view.findViewById(R.id.itemImage))
        }
    }

    private 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
    }
}
```

Create `ProductFragment` to display product search results:

```kotlin Kotlin icon=code theme={"system"}
class ProductFragment : Fragment(R.layout.fragment_items) {

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

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

        // Configure products view
        val productAdapter = ProductAdapter()
        view.findViewById<RecyclerView>(R.id.items).configure(productAdapter) // Configure the RecyclerView with the adapter
        connection += viewModel.productSearcher.connectHitsView(productAdapter) {
            it.hits.deserialize(Product.serializer())
        }

        // Run initial search
        viewModel.productSearcher.searchAsync()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        connection.clear()
    }
}
```

## Suggestions view

Create `Suggestion` data class corresponding the suggestions index hits:

```kotlin Kotlin icon=code theme={"system"}
import com.algolia.instantsearch.core.Indexable

@Serializable
data class Suggestion(
    val query: String,
    override val objectID: String,
    override val _highlightResult: JsonObject?
) : Indexable, Highlightable {

    val highlightedQuery: HighlightedString?
        get() = getHighlight("query")
}
```

Create `SuggestionAdapter` to display suggestions list:

```kotlin Kotlin icon=code theme={"system"}
class SuggestionAdapter(private val onSuggestionClick: ((Suggestion) -> Unit)) :
    ListAdapter<Suggestion, SuggestionViewHolder>(SuggestionAdapter),
    HitsView<Suggestion> {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        SuggestionViewHolder(parent.inflate(R.layout.list_item_suggestion))

    override fun onBindViewHolder(holder: SuggestionViewHolder, position: Int) {
        val item = getItem(position)
        holder.bind(item, onSuggestionClick)
    }

    override fun setHits(hits: List<Suggestion>) = submitList(hits)

    class SuggestionViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {

        fun bind(item: Suggestion, onClick: ((Suggestion) -> Unit)) {
            view.setOnClickListener { onClick(item) }
            view.findViewById<TextView>(R.id.itemName).text = item.highlightedQuery?.toSpannedString() ?: item.query
        }
    }

    companion object : DiffUtil.ItemCallback<Suggestion>() {
        override fun areItemsTheSame(oldItem: Suggestion, newItem: Suggestion) =
            oldItem.objectID == newItem.objectID

        override fun areContentsTheSame(oldItem: Suggestion, newItem: Suggestion): Boolean =
            oldItem == newItem
    }
}
```

Create `SuggestionFragment` to display suggestions' recycler view adapter,
and to notify `QuerySuggestionViewModel` when a suggestion selection is made:

```kotlin Kotlin icon=code theme={"system"}
class SuggestionFragment : Fragment(R.layout.fragment_items) {

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // Configure suggestions view
        val suggestionAdapter = SuggestionAdapter {
            // On suggestion click, update the
            viewModel.suggestions.value = it
        }
        view.findViewById<RecyclerView>(R.id.items).configure(suggestionAdapter) // Configure the RecyclerView with the adapter
        connection += viewModel.suggestionSearcher.connectHitsView(suggestionAdapter) {
            it.hits.deserialize(Suggestion.serializer())
        }

        // Run initial search
        viewModel.suggestionSearcher.searchAsync()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        connection.clear()
    }
}
```

## Setup layout

Finally, create `QuerySuggestionActivity` activity, and display `ProductFragment` and `SuggestionFragment` on `SearchBox`

```kotlin Kotlin icon=code theme={"system"}
class QuerySuggestionActivity : AppCompatActivity() {

    private val viewModel by viewModels<QuerySuggestionViewModel>()
    private val connection = ConnectionHandler()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_query_suggestion)

        // Setup search box
        val searchView = findViewById<SearchView>(R.id.searchView)
        val searchBoxView = SearchBoxViewAppCompat(searchView)
        connection += viewModel.searchBox.connectView(searchBoxView)

        // Switch fragments on search box focus
        searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
            if (hasFocus) showSuggestions() else showProducts()
        }

        // Observe suggestions
        viewModel.suggestions.observe(this) { searchBoxView.setText(it.query, true) }

        // Initially show products view
        showProducts()
    }

    private fun showSuggestions() {
        supportFragmentManager.commit {
            replace<SuggestionFragment>(R.id.container)
            setReorderingAllowed(true)
            addToBackStack("suggestions") // name can be null
        }
    }

    private fun showProducts() {
        supportFragmentManager.commit {
            replace<ProductFragment>(R.id.container)
            setReorderingAllowed(true)
            addToBackStack("products") // name can be null
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        connection.clear()
    }
}
```

## Going further

Your query suggestions search experience is now ready to use.
You can find a complete project in the [Android examples repository](https://github.com/algolia/instantsearch-android-examples).
