Concepts / Building Search UI / Create your own widgets
Aug. 20, 2019

Create Your Own Widgets

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

You are trying to create your own widget with Android InstantSearch and that’s awesome 🎉. But that also means that you couldn’t find the widgets or built-in options you were looking for. We’d love to hear about your use case as our mission with our InstantSearch libraries is to provide the best out-of-the-box experience. Don’t hesitate to send us 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.

Overview

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

We will 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, we 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.

We will 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: SearcherSingleIndex
) : 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 our widget, we 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: SearcherSingleIndex
): Connection {
    return SumSearchesConnectionSearcher(this, searcher)
}

Putting it all together

You just created your first custom widget, congratulations! 🎉

Now you can 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 = SearcherSingleIndex(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?