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?

ianhanniballake
  • 191,609
  • 30
  • 470
  • 443
user3292244
  • 453
  • 1
  • 4
  • 8

3 Answers3

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.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • You saved my Day – user3292244 Mar 09 '18 at 13:52
  • has anyone used PositionalDataSource? @user3292244 – Raghav Satyadev Mar 16 '18 at 06:45
  • It really helped. – Pardeep Kumar May 31 '18 at 09:13
  • 1
    Thats worked, but I'll get an random java.lang.IndexOutOfBoundsException: Inconsistency detected. on my recyclerview – abalta Oct 20 '18 at 02:09
  • 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. – Gurvinder Singh Jan 18 '19 at 09:22
  • When you are invalidating the data, do you see flickering ? – Gurvinder Singh Jan 18 '19 at 09:25
  • I don't think you should if the items are the same and the items are equal. – EpicPandaForce Jan 18 '19 at 10:30
  • How to do it with `ItemKeyedDataSource` ? – mallaudin Jul 31 '19 at 13:51
  • 1
    @mallaudin 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. – EpicPandaForce Jul 31 '19 at 15:36
  • Thanks @EpicPandaForce for your response. Yes. I am doing like this. Working fine now. – mallaudin Jul 31 '19 at 17:35
  • Made my day <3. Was thinking of something similar :) – coroutineDispatcher Jan 05 '20 at 17:12
  • 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. – Richard Dec 11 '20 at 10:08
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..

Deividas Strioga
  • 1,467
  • 16
  • 24
  • 1
    Yeah, this is also a possible solution with Paging lib. I was using the setup that was working with `LiveData>` or `RealmResults` 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! – EpicPandaForce Jun 25 '18 at 07:00
  • 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." – Dmitriy Puchkov Feb 08 '19 at 21:07
  • The real magic seems to be that the "unsubscribing / resubscribing" is what `Transformations.switchMap` does internally using a MediatorLiveData. – EpicPandaForce Jun 20 '19 at 14:23
0

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

Dennis Nguyen
  • 278
  • 2
  • 9