Guides / Building Search UI / Widgets

Create your own InstantSearch Android widgets

On this page

If none of the existing widgets fit your use case, you can implement your own.

You are trying to create your own widget with InstantSearch Android and that’s awesome. But that also means that you couldn’t find the widgets or built-in options you were looking for. Algolia would love to hear about your use case as the aim with the InstantSearch libraries is to provide the best out-of-the-box experience. Don’t hesitate to send a quick message explaining what you were trying to achieve either using the form at the end of that page or directly by submitting a feature request.

Creating a widget takes three steps:

  • Create the MyWidgetViewModel, containing the business logic for your widget.
  • Create a MyWidgetView interface, describing the rendering of the widget data.
    • Implement your view in a MyWidgetViewImpl that you’ll use.
  • Create the Connections between your ViewModel and the other components:
    • Create a MyWidgetConnectionView to connect your ViewModel to its View.
    • If it uses the Searcher, create a MyWidgetConnectionSearcher.
    • If it uses the FilterState, create a MyWidgetConnectionFilterState.

Example

You’ll build a widget that displays the number of searches made since it was last clicked.

Create the ViewModel

Our ViewModel will be quite straightforward: it stores a sum that can be incremented or reseted to 0. We will use InstantSearch’s SubscriptionValue to allow subscribing to changes of the sum’s value.

1
2
3
4
5
6
7
8
9
10
11
12
class SumSearchesViewModel {

    val sum = SubscriptionValue(0)

    fun increment() {
        sum.value++
    }

    fun reset() {
        sum.value = 0
    }
}

Create the View interface

To interact with the data in our ViewModel, you need a view than can display a number, and handle clicks for resetting.

1
2
3
4
5
interface SumSearchesView {

    fun setSum(sum: Int) // will be called on new sum
    var onReset: Callback<Unit>? // will hold the callback to reset the sum
}

Implementing our View

We can now implement a SumSearchesView: it should display the data received in setSum and trigger the onReset when clicked.

1
2
3
4
5
6
7
8
9
10
11
12
class SumSearchesViewImpl(val view: TextView) : SumSearchesView {

    init {
        view.setOnClickListener { onReset?.invoke(Unit) }
    }

    override fun setSum(sum: Int) {
        view.text = "$sum searches."
    }

    override var onReset: Callback<Unit>? = null
}

Create the ConnectionView

To link our ViewModel and its View, we will define a connection to describe what should happen when connecting (subscribe to sum and set the reset callback) and when disconnecting (unsubscribe to sum and remove the callback).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
data class SumSearchesConnectionView(
        private val viewModel: SumSearchesViewModel,
        private val view: SumSearchesView
) : ConnectionImpl() {

    private val updateViewSum: (Int) -> Unit = {
        view.setSum(it)
    }

    override fun connect() {
        super.connect()
        viewModel.sum.subscribePast(updateViewSum)
        view.onReset = { viewModel.reset() }
    }

    override fun disconnect() {
        super.disconnect()
        viewModel.sum.unsubscribe(updateViewSum)
        view.onReset = null
    }
}

Create the ConnectionSearcher

Because our widget needs to be aware of searches to count them, it needs to be connected to a Searcher.

Subscribe to its response to call increment() on every new search response.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
data class SumSearchesConnectionSearcher(
        private val viewModel: SumSearchesViewModel,
        private val searcher: HitsSearcher
) : ConnectionImpl() {

    private val updateSum: (ResponseSearch?) -> Unit = {
        viewModel.increment()
    }

    override fun connect() {
        super.connect()
        searcher.response.subscribe(updateSum)
    }

    override fun disconnect() {
        super.disconnect()
        searcher.response.unsubscribe(updateSum)
    }
}

Convenient functions

To simplify usage of the widget, you will create two extension functions that connect the ViewModel to other components:

1
2
3
4
5
6
7
8
9
10
11
fun SumSearchesViewModel.connectView(
        view: SumSearchesView
): Connection {
    return SumSearchesConnectionView(this, view)
}

fun SumSearchesViewModel.connectSearcher(
        searcher: HitsSearcher
): Connection {
    return SumSearchesConnectionSearcher(this, searcher)
}

Final touches

You just created your first custom widget and you can now use it in your application like any other widget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
val searcher = HitsSearcher(client, index) // Initialize your Searcher as usual
val view = TextView(context) // Create or find the view you want to use

// Create a connectionHandler to hold all connections
val connection = ConnectionHandler()

// Create your ViewModel and View implementation
val viewModel = SumSearchesViewModel()
val sumView = SumSearchesViewImpl(view)

// Connect your ViewModel to start displaying the count of searches
connection += viewModel.connectSearcher(searcher)
connection += viewModel.connectView(sumView)


// When you want to disconnect everything and ensure no memory leak
// for example in your Activity's onDestroy()`
connection.disconnect()
Did you find this page helpful?