Building a search screen with Jetpack Compose

Harry
ProAndroidDev
Published in
7 min readSep 17, 2023

--

Photo by Andreas Gücklhorn on Unsplash

In this article, we’ll be discussing the recommended approach for building a search screen in Jetpack Compose and the motivations behind each design decision.

This article assumes a good understanding of Jetpack Compose fundamentals and a familiarity with the recommended Android architecture components.

Getting started

To perform a search, we first need a search query. A conventional way to store this query would be to create a private MutableStateFlow in a ViewModel and expose a read-only StateFlow for observation within a Compose screen.

In the majority of cases, this solution works as expected. Unfortunately, using a reactive stream like StateFlow to store the text of a Compose TextField can lead to unexpected behaviour — such as characters appearing in the wrong places or being skipped entirely.

Issue when typing “Search query”

This strange behaviour is caused by the async nature of StateFlow producing inconsistencies between the multiple internal states of a TextField.

If you would like to delve further into this issue, please refer to the Medium article below.

Search query solution

How can we store the text of a TextField if a MutableStateFlow cannot be used? The suggested method is to instead create a MutableState variable with a private setter.

This approach ensures that only the ViewModel is allowed to modify the search query, while still making it available for observation as a read-only state within a Compose screen. This matches the functionality and pattern of the previously invalid MutableStateFlow solution.

Search list

With the search query handled, we now need a list of items to perform the search logic on. In this article, a search will be performed on a list of movies. To model this, we create a simple data class.

We then create a list of movies in the ViewModel using this data class. In a production Android app, this data is typically supplied by a repository function or a domain use case in the form of a flow. For the sake of clarity in this example, we instead create a moviesFlow to emulate this data retrieval.

Producing search results

With a search query and a list of movies, the next task is to perform the search logic. For this, we create a searchResults value in the ViewModel that contains a list of movie search results based on the search query.

To start, we use the snapshotFlow API to transform the MutableState search query into a flow.

The combine operator is then used with the moviesFlow to obtain the most recently emitted values from each flow. The combine operator also waits for both flows to emit at least one value before it starts combining them.

We can now implement the following search logic:

  • If the search query has a value, filter the movie list by names matching the search query
  • If the search query is empty, do not filter the movie list

To transform the search results into a state value that is optimised for collection in a Compose screen, we use the stateIn operator. To use this operator, we specify which coroutine scope to start the flow in and the initial value of the state.

We also set the strategy that controls when sharing is started and stopped. In this case, we set the flow to only remain active when there is at least one subscribed collector. When the last collector of the flow unsubscribes, we wait five seconds before stopping the upstream flow in case the collector only went away temporarily — this avoids restarting the upstream flow in certain situations such as configuration changes.

Updating the search query

The last task the ViewModel should perform is to handle requests from the screen to update the search query value.

Stateful search screen

With the ViewModel set up, the next step is to create the search screen. It is a recommended practice to create both a stateful and a stateless version of each screen in your Compose app, as it makes them more reusable and easier to test.

The stateful screen retrieves state and state updating functions from the ViewModel, which are then passed into the stateless screen. This technique is known as state hoisting.

We first create the stateful version of the search screen.

The collectAsStateWithLifecycle API is used to collect searchResults in a lifecycle-aware manner. To make use of this API, add the following dependency to your project.

This API and the advantages behind it are explained in the article below — the key benefit is a boost to performance by not keeping app resources alive unnecessarily.

Stateless search screen

We then create the stateless version of the search screen.

To create the search bar, we make use of the SearchBar composable offered by Compose Material 3. We supply the SearchBar with the state values and state update functions, alongside adding a search icon and some hint text. For the sake of clarity in this example, the search hint has not been extracted as a string resource.

Search results

The next step is to display the search results to the user. To start, we create a MovieListItem composable to display the details of each movie.

We then use this MovieListItem within a LazyColumn to display the search results in a vertical list, alongside applying some padding to improve the list’s appearance. The movie data is pulled from the supplied searchResults variable.

To improve the performance of the LazyColumn, a count of the number of items is provided, alongside a unique key. Providing a count and a stable key for each item in the LazyColumn allows Compose to avoid unnecessary recompositions and offers a performance boost.

The last step is to pass this LazyColumn composable to the content parameter of the SearchBar.

Navigating to the screen

The stateful screen composable now needs to be reachable within the app. As this is a new project, we can simply add the screen to the main activity class.

If this screen is being implemented as part of a pre-existing app, you can instead integrate it into your existing navigation logic.

Previewing the screen

Having a stateless screen also makes creating a Compose preview of the screen much easier, as we simply pass in the required state.

We then build the project and open the preview pane to view the created search screen.

Running the app

With the app completed, it can now be deployed to an emulator or physical device. The app’s functionality is shown below — typing in the search bar filters the movie list by movie name. If the search text is empty, a hint is shown to the user and all movies are displayed instead.

Further improvements

There are a few additional improvements we can make to enhance the user experience of the search screen.

The first is to add a button to the search bar to clear the query, instead of making the user manually delete each character. We can use the previously defined state and event functions to achieve this functionality.

The functionality of this new button is shown below.

Next, we can display an empty state to the user when there are no movies that match the search query, instead of displaying a potentially confusing blank screen. For this, we define two text styles and use them to create an empty state composable.

We then update the search results list to display the empty state to the user if there are no results.

With this addition, we have removed a potential source of confusion for the user.

Finally, we can improve the feel of the search screen by hiding the software keyboard if the user presses the keyboard search action. To add this functionality, we obtain the current keyboard controller and use it to hide the keyboard when the onSearch function is called.

The user now has a more natural way to close the keyboard when they have finished typing a search query.

With these three improvements implemented, the search screen now offers a much smoother experience for the user.

Conclusion

That wraps up this article! I hope it has given you a better understanding of the recommended approach for building a search screen in Jetpack Compose.

You can find my Jetpack Compose projects on GitHub — feel free to reach out with any questions or feedback.

Happy coding!

--

--