Getting started with imperative UI
On this page
This guide walks you through the few steps needed to start a project with InstantSearch Android. It starts from an empty Android project, and creates a full search experience from scratch.
This search experience includes:
- A list to display search results
- A search box to type your query
- Statistics about the current search
- A facet list for filtering results
Installation
To use Algolia with InstantSearch Android, you need an Algolia account. You can sign up for a new account, or use the following credentials:
- Application ID:
latency
- Search API Key:
1f6fd3a6fb973cb08419fe7d288fa4db
- Index name:
instant_search
These credentials will let you use a preloaded dataset of products appropriate for this guide.
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:
1
2
implementation 'com.algolia:instantsearch-android:3.+'
implementation 'com.algolia:instantsearch-android-paging3:3.+'
This guide uses InstantSearch Android with Android Architecture Components, so you also need to add the following dependencies:
1
2
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.+'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.+'
To perform network operations in your application, AndroidManifest.xml
must include the following permissions:
1
<uses-permission android:name="android.permission.INTERNET" />
Setup kotlinx.serialization
by adding the serialization plugin to your build.gradle
:
1
2
3
4
plugins {
// ...
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10'
}
Implementation
Architecture overview
MainActivity
: This activity controls the fragment currently displayed.MyViewModel
: AViewModel
from Android Architecture Components. The business logic lives here.ProductFragment
: This fragment displays a list of search results in aRecyclerView
, aSearchView
input, and aStats
indicator.FacetFragment
: This fragment displays a list of facets to filter your search results.
Initializing a Searcher
The central part of your search experience is the Searcher
. The Searcher
performs search requests and obtains search results. Most InstantSearch components are connected with the Searcher
.
In this tutorial you will only target one index, so instantiate a HitsSearcher
with the proper credentials.
Go to MyViewModel.kt
file and add the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyViewModel : ViewModel() {
val searcher = HitsSearcher(
applicationID = ApplicationID("latency"),
apiKey = APIKey("1f6fd3a6fb973cb08419fe7d288fa4db"),
indexName = 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.
Displaying your 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.
The first step to display your results is to create a LiveData
object, which holds a PagedList
of Product
.
Create the Product
data class which contains a single name
field.
1
2
3
4
@Serializable
data class Product(
val name: String
)
Create the product_item.xml
file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<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>
Create the ProductViewHolder
to bind a Product
item to a RecyclerView.ViewHolder
.
1
2
3
4
5
6
7
8
class ProductViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val itemName = view.findViewById<TextView>(R.id.itemName)
fun bind(product: Product) {
itemName.text = product.name
}
}
Create a ProductAdapter
by extending PagingDataAdapter
. The ProductAdapter
binds products to the ViewHolder
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
}
}
You can now use the Paginator
with your searcher. Do this in your ViewModel
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()
}
}
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?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>
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
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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:
1
2
3
4
5
<?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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
.
Searching your data: SearchBox
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
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 application, 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 achieve this in a few lines.
Add a StatsConnector
to your MyViewModel
, and connect it with a ConnectionHandler
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?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>
Finally, connect the StatsConnector
to a StatsTextView
in your ProductFragment
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 application and see your new search experience in action.
Filter your data: FacetList
Filtering search results helps your users find exactly 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.
1
2
3
4
5
6
7
8
9
10
<?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
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<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
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.algolia.search.model.search.Facet
class MyFacetListViewHolder(view: View) : FacetListViewHolder(view) {
override fun bind(facet: Facet, 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 category
.
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 theFilterState
, and applies its filters with every search. - The
FilterState
connects to your productsPaginator
to invalidate search results when new filter are applied. - Finally, the
facetList
connects to its adapter.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class MyViewModel : ViewModel() {
// Client, Searcher...
// Products
// Stats
// ...
val filterState = FilterState()
val facetList = FacetListConnector(
searcher = searcher,
filterState = filterState,
attribute = 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
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?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
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 application to see your advanced search experience. This experience helps your users to filter search results and find exactly 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 query. 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
.
First, define a highlightedName
field to retrieve the highlighted value for the name
attribute.
1
2
3
4
5
6
7
8
9
@Serializable
data class Product(
val name: String,
override val _highlightResult: JsonObject?
) : Highlightable {
public val highlightedName: HighlightedString?
get() = getHighlight(Attribute("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.
1
2
3
4
5
6
7
8
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 how many records are returned and why they’re relevant to the query.
You can find the full source code in the GitHub repository.
Going further
This is only an introduction to what you can do with InstantSearch Android: check out the widget showcase to see more components.