Hector
Hector

Reputation: 5694

Android Room Paging3 correct approach for dynamic filtering

I am investigating the new Android Room Paging library

   implementation "androidx.paging:paging-runtime-ktx:3.0.0-alpha09"

My source database tables approx 10,000 rows and I filter by the first character of the name field as follows:-

DAO

@Query("SELECT * from citation_style WHERE citation_style_name LIKE :startsWith ORDER BY citation_style_name ASC")
fun fetch(startsWith: String): PagingSource<Int, CitationStyleDO>

Repository

fun fetch(startsWith: String): Flow<PagingData<CitationStyleDO>> {
    return Pager(
        PagingConfig(pageSize = 60, prefetchDistance = 30, enablePlaceholders = false, maxSize = 200)
    ) { database.citationStyleDao().fetch("$startsWith%") }.flow
}

ViewModel

fun fetch(startsWith: String): Flow<PagingData<CitationStyleDO>> {
    return repository.fetch(startsWith).cachedIn(viewModelScope)
}

Fragment

override fun onStartsWithClicked(startsWith: String) {
    lifecycleScope.launch {
        viewModel.fetch(startsWith).collectLatest { adapter.submitData(it) }
    }
}

Is this the correct approach of repeatedly using lifecycleScope.launch {...} each time the Starts With character is changed?

Should I be map{} or switchMap{} triggered by a MutabaleLiveData<String> for StartwWith?

Upvotes: 7

Views: 3627

Answers (1)

dlam
dlam

Reputation: 3895

This won't work, because submitData doesn't return until PagingData is invalidated. You might run into race scenarios where you have multiple jobs launched where PagingDataAdapter is trying to collect from multiple PagingData.

The more "Flow" way would be to turn your fetch calls into a flow and combine that with your Flow<PagingData>, which will automatically propagate cancellation every time your query changes.

A couple other things:

It's recommended to let Paging do the filtering for you, as this way you can avoid re-fetching from DB every time your search changes, and also you can lean on Paging to handle configuration changes and restore state.

You should use viewLifecycleOwner instead of lifecycleScope directly, because you don't want paging to do work after the fragment's view is destroyed

e.g.,

ViewModel

val queryFlow = MutableStateFlow("init_query")
val pagingDataFlow = Pager(...) {
        dao.pagingSource()
    }.flow
    // This multicasts, to prevent combine from refetching
    .cachedIn(viewModelScope)
    .combine(queryFlow) { pagingData, query -> 
        pagingData.filter { it.startsWith(query)
    }
    // Optionally call .cachedIn() here a second time to cache the filtered results. 

Fragment

override fun onStartsWithClicked(startsWith: String) {
    viewModel.queryFlow = startsWith
}

override fun onViewCreated(...) {
     viewLifecycleOwner.lifecycleScope.launch {
viewModel.pagingDataFlow.collectLatest { adapter.submitData(it) }
}

Note: You can definitely use Room to do the filtering if you want, probably the right way there is to .flatMapLatest on the queryFlow and return a new Pager each tine, and pass the query term to dao function that returns a PagingSource

ViewModel

queryFlow.flatMapLatest { query ->
    Pager(...) { dao.pagingSource(query) }
        .cachedIn(...)
}

Upvotes: 9

Related Questions