Searching Paged Data In Jetpack Compose
Tags: kotlin, jetpackcompose, android, paging3
Published: 2022-11-06 22:57:56

---
title: Searching Paged Data In Jetpack Compose
published: true
date: 2022-11-06 20:15:42 UTC
tags: kotlin, jetpackcompose, android, paging3
canonical_url: https://www.euantorano.co.uk/posts/2022/11/06/searching-paged-data-jetpack-compose/
---

I've recently created a new Android app using the [Jetpack Compose UI App Development Toolkit](https://developer.android.com/jetpack/compose) which consisted of a list of items which needed to be searchable. As the list was quite long, and the API I was talking to implemented support for pagination, I made use of the [Paging library](https://developer.android.com/topic/libraries/architecture/paging/v3-overview) to lazy load items into the list as it scrolled.

Nice and easy so far. Now came the time to implement searching for items. I wanted to be able to toggle a search box at the top of the page and have the list of items filter itself based upon the search term as the user typed. After a little trial and error and a good while searching through Google's documentation given how rarely I work with Android, I came up with a nice and simple solution.

The end result that we're aiming for is something like this:

![A demo of the search in action searching for products that match the search term 'Phone' within a sample application.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/59024y3cys9fgtjc9ejp.gif)

My ViewModel layers all use [Kotlin Flows](https://developer.android.com/kotlin/flow) to store state, meaning my basic view model for the paged data looked something like the following:

```kotlin
@HiltViewModel
class ProductListViewModel @Inject constructor(
    private val repository: Repository
): ViewModel() {
    val products = Pager(
        PagingConfig(
            pageSize = 10,
            enablePlaceholders = false,
        )
    ) {
        ProductPagingSource(
            repository = repository,
            search = query,
        )
    }.flow.cachedIn(viewModelScope)
}
```

_(Note, I use [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) to inject dependencies into my view models, but that's not important for this post)_

To add searching to this, I needed to add a few new properties and a couple of functions:

1. A boolean to toggle whether the search box is visible.
2. A string to contain the current search term.
3. Functions to toggle the search box visibility, and to set the search term.

My view model now looked like this:

```kotlin
@HiltViewModel
class ProductListViewModel @Inject constructor(
    private val repository: Repository
): ViewModel() {
    private val _search = MutableStateFlow("")

    val search = _search.asStateFlow()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(),
            initialValue = "",
        )

    private val _isSearchShowing = MutableStateFlow(false)

    val isSearchShowing = _isSearchShowing.asStateFlow()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(),
            initialValue = false,
        )

    val products = Pager(
        PagingConfig(
            pageSize = 10,
            enablePlaceholders = false,
        )
    ) {
        ProductPagingSource(
            repository = repository,
            search = query,
        )
    }.flow.cachedIn(viewModelScope)

    fun setSearch(query: String) {
        _search.value = query
    }

    fun toggleIsSearchShowing() {
        _isSearchShowing.value = !_isSearchShowing.value
    }
}
```

Now we just need to update the paged data when the search term changes. Luckily, this is extremely easy when working with flows as we can use a flow transformation (namely [`flatMapLatest`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flat-map-latest.html)) so that when the search term changes a new flow is emitted:

```kotlin
@HiltViewModel
class ProductListViewModel @Inject constructor(
    private val repository: Repository
): ViewModel() {
    private val _search = MutableStateFlow("")

    val search = _search.asStateFlow()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(),
            initialValue = "",
        )

    private val _isSearchShowing = MutableStateFlow(false)

    val isSearchShowing = _isSearchShowing.asStateFlow()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(),
            initialValue = false,
        )

    val productsSearchResults = search.debounce(300.milliseconds).flatMapLatest { query ->
        Pager(
            PagingConfig(
                pageSize = 10,
                enablePlaceholders = false,
            )
        ) {
            ProductPagingSource(
                repository = repository,
                search = query,
            )
        }.flow.cachedIn(viewModelScope)
    }


    fun setSearch(query: String) {
        _search.value = query
    }

    fun toggleIsSearchShowing() {
        _isSearchShowing.value = !_isSearchShowing.value
    }
}
```

We also apply the [`debounce`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/debounce.html) transformation so that as the user is typing we don't constantly refresh the paging data. In this case, I set the debounce interval to 300 milliseconds.

All that's left to do is to hook the view model up to a view - an exercise left to the reader, though you can [see my implementation here](https://github.com/euantorano/compose-paging-with-search-example/blob/main/app/src/main/java/uk/co/euantorano/example/compose_paging/ui/screens/ProductListScreen.kt).

A full sample project is [available on GitHub](https://github.com/euantorano/compose-paging-with-search-example), which ties this approach together with dummy product data coming from a web request.