Paging Library Filter/Search

我们两清 提交于 2019-11-30 10:24:24

问题


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?


回答1:


EDIT from 2019: hold on, I think you might be able to solve this with a MediatorLiveData.

Specifically Transformations.switchMap and some additional magic.

Currently I was using

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 MutableLiveData<String> filterText = new MutableLiveData<>();

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. If we want to cache the LiveDatas, then we can do in the anonymous instance that we pass to the method.

    data = Transformations.switchMap(
            filterText, new Function<String, LiveData<List<T>>>() {
                private Map<String, LiveData<List<T>>> cachedLiveData = new HashMap<>();

                @Override
                public LiveData<List<T>> apply(String input) {
                    // ...
                }
            }



ORIGINAL ANSWERS (they are outdated)

EDIT: actually. While this makes sense for a regular LiveData<?>, with Paging, you can actually parameterize the Factory and then invalidate the Datasource and get a new datasource evaluated for free. Without recreating the query holder itself.

So the method mentioned in the other answer is a better option when you're using Paging.


ORIGINAL ANSWER:

You know how you have the adapter like this:

public class TaskAdapter
        extends PagedListAdapter<Task, TaskAdapter.ViewHolder> {
    public TaskAdapter() {
        super(Task.DIFF_ITEM_CALLBACK);
    }

In the ViewModel you set up a live paged list and expose it:

private LiveData<PagedList<Task>> liveResults;

public TaskViewModel() {
    liveResults = new LivePagedListBuilder<>(taskDao.tasksSortedByDate(),
        new PagedList.Config.Builder() //
              .setPageSize(20) //
              .setPrefetchDistance(20) //
              .setEnablePlaceholders(true) //
              .build())
            .setInitialLoadKey(0)
            .build();

Then you observe the paged list in the ViewModel and set it to the adapter:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    viewModel.getTasks().observe(this, pagedList -> {
        //noinspection Convert2MethodRef
        taskAdapter.submitList(pagedList); //used to be `setList`
    });

Well, the tricky thing is that if you want to make it parametric, then you have to replace the following here, and make the view be able to observe it:

    liveResults = new LivePagedListBuilder<>(userDao.usersByName(input) // <-- !!

So you must replace the LiveData. o_o

What you can do in that case is remove the observers from the existing LiveData, replace it with new LiveData, and start observing it.

private void startListening() {
    viewModel.getTasks().observe(this, pagedList -> {
        //noinspection Convert2MethodRef
        taskAdapter.submitList(pagedList); // used to be `setList`
    });
}

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    startListening();
}

@OnTextChanged(R.id.edit_text)
public void onTextChanged(Editable editable) {
    String username = editable.toString();
    replaceSubscription(userName);
}

private void replaceSubscription(String userName) {
    viewModel.replaceSubscription(this, userName);
    startListening();
}

and

public class UserViewModel extends ViewModel {
    private LiveData<PagedList<User>> liveResults;

    private String userName;

    private LiveData<PagedList<User>> createFilteredUsers(String userName) {
       // TODO: handle if `null` and load all data instead
       return new LivePagedListBuilder<>(userDao.usersByName(userName),
            new PagedList.Config.Builder() //
                  .setPageSize(20) //
                  .setPrefetchDistance(20) //
                  .setEnablePlaceholders(true) //
                  .build())
                .setInitialLoadKey(0)
                .build();
    }

    public UserViewModel(UserDao userDao, @Nullable String userName) { // null or restored, from ViewModelProviders.of(Factory)
        liveResults = createFilteredUsers(userName);
    }

    public void replaceSubscription(LifecycleOwner lifecycleOwner, String userName) {
        this.userName = userName;
        liveResults.removeObservers(lifecycleOwner);
        liveResults = createFilteredUsers(userName);
    }
}



回答2:


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..



来源:https://stackoverflow.com/questions/49192540/paging-library-filter-search

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!