Paging Library Filter/Search
Asked Answered
D

3

40

I am using the Android Paging Library like described here: https://developer.android.com/topic/libraries/architecture/paging.html

But i also have an EditText for searching Users by Name.

How can i filter the results from the Paging library to display only matching Users?

Disagreeable answered 9/3, 2018 at 11:16 Comment(1)
The question's answer is here: https://mcmap.net/q/408447/-android-implement-search-with-view-model-and-live-data using switchMap and MutableLiveDataAttaboy
K
42

You can solve this with a MediatorLiveData.

Specifically Transformations.switchMap.

// original code, improved later
public void reloadTasks() {
    if(liveResults != null) {
        liveResults.removeObserver(this);
    }
    liveResults = getFilteredResults();
    liveResults.observeForever(this);
}

But if you think about it, you should be able to solve this without use of observeForever, especially if we consider that switchMap is also doing something similar.

So what we need is a LiveData<SelectedOption> that is switch-mapped to the LiveData<PagedList<T>> that we need.

private final MutableLiveData<String> filterText = savedStateHandle.getLiveData("filterText")

private final LiveData<List<T>> data;

public MyViewModel() {
    data = Transformations.switchMap(
            filterText,
            (input) -> { 
                if(input == null || input.equals("")) { 
                    return repository.getData(); 
                } else { 
                    return repository.getFilteredData(input); }
                }
            });
  }

  public LiveData<List<T>> getData() {
      return data;
  }

This way the actual changes from one to another are handled by a MediatorLiveData.

Knobloch answered 9/3, 2018 at 12:1 Comment(12)
You saved my DayDisagreeable
has anyone used PositionalDataSource? @DisagreeableHipparchus
It really helped.Standley
Thats worked, but I'll get an random java.lang.IndexOutOfBoundsException: Inconsistency detected. on my recyclerviewJermyn
I am doing the same way, but there is an issue with continuous insertions. Whenever new item is inserted in the db my list is notified and sorted list is observed in the observer and list flickers if any item is inserted at the top. If insertion is at the bottom list view is fine, but if item inserted the top then list flickers.Unclad
When you are invalidating the data, do you see flickering ?Unclad
I don't think you should if the items are the same and the items are equal.Knobloch
How to do it with ItemKeyedDataSource ?Cutshall
@Cutshall Move in the filter argument into the datasource through constructor argument, then if you need to change it, invalidate the data source. Make sure you update the filter text inside the datasource factory before you call invalidate on the datasource.Knobloch
Thanks @Knobloch for your response. Yes. I am doing like this. Working fine now.Cutshall
Made my day <3. Was thinking of something similar :)Serf
This works! Thanks, but I think that you should have included the fact that this only works by asking the repository layer to re-query a new set of data with the filter included. Although the answer was accepted, the question is rather ambiguous and might have referred to filtering the data after it is queried; if this is the case, there isn't any possible way afaik.Divulgate
S
25

I have used an approach similar to as answered by EpicPandaForce. While it is working, this subscribing/unsubscribing seems tedious. I have started using another DB than Room, so I needed to create my own DataSource.Factory anyway. Apparently it is possible to invalidate a current DataSource and DataSource.Factory creates a new DataSource, that is where I use the search parameter.

My DataSource.Factory:

class SweetSearchDataSourceFactory(private val box: Box<SweetDb>) :
DataSource.Factory<Int, SweetUi>() {

var query = ""

override fun create(): DataSource<Int, SweetUi> {
    val lazyList = box.query().contains(SweetDb_.name, query).build().findLazyCached()
    return SweetSearchDataSource(lazyList).map { SweetUi(it) }
}

fun search(text: String) {
    query = text
}
}

I am using ObjectBox here, but you can just return your room DAO query on create (I guess as it already is a DataSourceFactory, call its own create).

I did not test it, but this might work:

class SweetSearchDataSourceFactory(private val dao: SweetsDao) :
DataSource.Factory<Int, SweetUi>() {

var query = ""

override fun create(): DataSource<Int, SweetUi> {
    return dao.searchSweets(query).map { SweetUi(it) }.create()
}

fun search(text: String) {
    query = text
}
}

Of course one can just pass a Factory already with the query from dao.

ViewModel:

class SweetsSearchListViewModel
@Inject constructor(
private val dataSourceFactory: SweetSearchDataSourceFactory
) : BaseViewModel() {

companion object {
    private const val INITIAL_LOAD_KEY = 0
    private const val PAGE_SIZE = 10
    private const val PREFETCH_DISTANCE = 20
}

lateinit var sweets: LiveData<PagedList<SweetUi>>

init {
    val config = PagedList.Config.Builder()
        .setPageSize(PAGE_SIZE)
        .setPrefetchDistance(PREFETCH_DISTANCE)
        .setEnablePlaceholders(true)
        .build()

    sweets = LivePagedListBuilder(dataSourceFactory, config).build()
}

fun searchSweets(text: String) {
    dataSourceFactory.search(text)
    sweets.value?.dataSource?.invalidate()
}
}

However the search query is received, just call searchSweets on ViewModel. It sets search query in the Factory, then invalidates the DataSource. In turn, create is called in the Factory and new instance of DataSource is created with new query and passed to existing LiveData under the hood..

Sexist answered 20/6, 2018 at 16:7 Comment(3)
Yeah, this is also a possible solution with Paging lib. I was using the setup that was working with LiveData<List<T>> or RealmResults<T> or any similar Observable collections. But here you can parameterize the DataSource.Factory and invalidate the data source and you'll receive a new datasource with the new, filtered setup. Good choice for Paging!Knobloch
Argument for your decision from documentation for DataSource class. "...If the underlying data set is modified, a new PagedList / DataSource pair must be created to represent the new data."Ribwort
The real magic seems to be that the "unsubscribing / resubscribing" is what Transformations.switchMap does internally using a MediatorLiveData.Knobloch
D
-1

You can go with other answers above, but here is another way to do that: You can make the Factory to produce a different DataSource based on your demand. This is how it's done: In your DataSource.Factory class, provide setters for parameters needed to initialize the YourDataSource

private String searchText;
...
public void setSearchText(String newSearchText){
    this.searchText = newSearchText;
}
@NonNull
@Override
public DataSource<Integer, SearchItem> create() {
    YourDataSource dataSource = new YourDataSource(searchText); //create DataSource with parameter you provided
    return dataSource;
}

When users input new search text, let your ViewModel class to set the new search text and then call invalidated on the DataSource. In your Activity/Fragment:

yourViewModel.setNewSearchText(searchText); //set new text when user searchs for a text

In your ViewModel, define that method to update the Factory class's searchText:

public void setNewSearchText(String newText){
   //you have to call this statement to update the searchText in yourDataSourceFactory first
   yourDataSourceFactory.setSearchText(newText);
   searchPagedList.getValue().getDataSource().invalidate(); //notify yourDataSourceFactory to create new DataSource for searchPagedList
}

When DataSource is invalidated, DataSource.Factory will call its create() method to create newly DataSource with the newText value you have set. Results will be the same

Defray answered 16/4, 2021 at 5:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.