Paging Library Filter/Search - android

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?

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.

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

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

Related

Room Database entries in RecyclerView

I want to show Room Database entries in a RecyclerView. So far I have the Room skeleton and I can show some dummy content (not from Room) in the RecyclerView:
However I have struggles showing Room DB entries instead of the dummy content. In Arduino the EEPROM I/O stuff used to be almost a oneliner but within Android Room this conceptually easy task seems to be a code-intense and not-so-forward challenge. This brings me to my first question:
1) As in my case the database is pretty slim and simple, is there any simpler approach than Room using less overhead and classes?
Regarding the Room approach, I believe that I am pretty close. I have difficulties implementing the following:
2) How can I substitute the for-loop in init DummyContent by the Room-DB entries (allJumps from ViewModel)?
Here is what I got so far (I didn't post anything below the ViewModel such as Repository and DAO's as it should not of interest right now):
DummyItems (dummy contents to be replaced by Room DB entries)
object DummyContent {
// An array of sample (dummy) items.
val ITEMS: MutableList<DummyItem> = ArrayList()
// A map of sample (dummy) items, by ID.
val ITEM_MAP: MutableMap<String, DummyItem> = HashMap()
private val COUNT = 25
init {
// Add some sample items.
// TO BE REPLACED BY ROOM DB ENTRIES <----------------------------------------------------
for (i in 1..COUNT) {
addItem(createDummyItem(i))
}
}
private fun addItem(item: DummyItem) {
ITEMS.add(item)
ITEM_MAP.put(item.id, item)
}
private fun createDummyItem(position: Int): DummyItem {
return DummyItem(position.toString(), "Item " + position, makeDetails(position))
}
private fun makeDetails(position: Int): String {
val builder = StringBuilder()
builder.append("Details about Item: ").append(position)
for (i in 0..position - 1) {
builder.append("\nMore details information here.")
}
return builder.toString()
}
// A dummy item representing a piece of content.
data class DummyItem(val id: String, val content: String, val details: String) {
override fun toString(): String = content
}
}
allJumps / JumpData
// allJumps is of type LiveData<List<JumpData>>
#Entity
data class JumpData (
#PrimaryKey var jumpNumber: Int,
var location: String?
}
ViewModel
class JumpViewModel(application: Application) : AndroidViewModel(application) {
// The ViewModel maintains a reference to the repository to get data.
private val repository: JumpRepository
// LiveData gives us updated words when they change.
val allJumps: LiveData<List<JumpData>>
init {
// Gets reference to WordDao from WordRoomDatabase to construct
// the correct WordRepository.
val jumpsDao = JumpRoomDatabase.getDatabase(application, viewModelScope).jumpDao()
repository = JumpRepository(jumpsDao)
allJumps = repository.allJumps // OF INTEREST <----------------------------------------------------
}
fun insert(jump: JumpData) = viewModelScope.launch {
repository.insert(jump)
}
fun getJumps() : LiveData<List<JumpData>> {
return allJumps
}
}
You can try to add this to object DummyContent
object DummyContent {
val jumpsLiveData = MutableLiveData<List<JumpData>>()
private val observedLiveData: LiveData<List<JumpData>>? = null
private val dataObserver = object : Observer<List<JumpData>> {
override fun onChanged(newList: List<JumpData>) {
// Do something with new data set
}
}
fun observeJumpsData(jumpsLiveData: LiveData<List<JumpData>>) {
observedLiveData?.removeObserver(dataObserver)
observedLiveData = jumpsLiveData.apply {
observeForever(dataObserver)
}
}
}
And this to viewModel's init block:
init {
val jumpsDao = JumpRoomDatabase.getDatabase(application, viewModelScope).jumpDao()
repository = JumpRepository(jumpsDao)
allJumps = repository.allJumps
DummyContent.observeJumpsData(getJumps())
}
By this code, DummyContent will automatically subscribe to new data after ViewModel creation
And in 'Activity', where you created RecyclerView, add this text to end of onCreate:
override fun onCreate(savedState: Bundle?) {
DummyContent.jumpsLiveData.observe(this, Observer {
recyclerAdapter.changeItemsList(it)
}
}
changeItemsList - method that changes your recycler's data, i believe, you already created it

Apply Transforamtion.map upon a LiveData<PagedList of objects>

Inside my ViewModel class i have defined my paged list configuration
private val pagedListConfig: PagedList.Config = PagedList.Config.Builder().apply {
setEnablePlaceholders(true)
setInitialLoadSizeHint(10)
setPageSize(10)
}.build()
After that i retrieve from my Room database the messages that i want to show in my chatRoom Activity given to the groupId which i also take it from database and i make a switchMap Transformation
private var groupChatItem = MutableLiveData<GroupChatItem>()
var chatRoomGroupMessages: LiveData<PagedList<MessageWithMsgQueueAccount>> =
Transformations.switchMap(groupChatItem) {
it?.let {
LivePagedListBuilder(
messagesRepository.retrieveChatRoomGroupMessages(
chatRoomServerId,
it.groupId
), pagedListConfig
).build()
}
}
All good up to now. Here i want to transform the List to expose a list of List, so basically i want to convert every element to a element through a function.
So what i need is a Transformation.map() to the first LiveData so i can change it to another LiveData. But the problem is that i want to do it with Paged List. How can i do this?
var messageChatItems: LiveData<List<MessageChatItem>> = Transformations.map(chatRoomGroupMessages, messageChatItem -> {
// Here is where i need to call the function
})
fun convertGroupItemToMessageItem(): MessageChatItem {
// here i make the convertion
}
So i get this to work as below
var chatRoomGroupMessages: LiveData<PagedList<MessageItem>> = Transformations.switchMap(groupChatItem) {
it?.let {
// Here is the messages from database
val groupItemFactory = messagesRepository.getChatRoomMessages()
.map { messageItem: ChatMessageItem? ->
// Here i transform them
toMessageChatItem(messageItem, it.accountId) }
LivePagedListBuilder(
groupItemFactory, pagedListConfig
).build()
}
}
And the transform function is the "toMessageItem()" function

Convert LiveData to MutableLiveData

Apparently, Room is not able to handle MutableLiveData and we have to stick to LiveData as it returns the following error:
error: Not sure how to convert a Cursor to this method's return type
I created a "custom" MutableLiveData in my DB helper this way:
class ProfileRepository #Inject internal constructor(private val profileDao: ProfileDao): ProfileRepo{
override fun insertProfile(profile: Profile){
profileDao.insertProfile(profile)
}
val mutableLiveData by lazy { MutableProfileLiveData() }
override fun loadMutableProfileLiveData(): MutableLiveData<Profile> = mutableLiveData
inner class MutableProfileLiveData: MutableLiveData<Profile>(){
override fun postValue(value: Profile?) {
value?.let { insertProfile(it) }
super.postValue(value)
}
override fun setValue(value: Profile?) {
value?.let { insertProfile(it) }
super.setValue(value)
}
override fun getValue(): Profile? {
return profileDao.loadProfileLiveData().getValue()
}
}
}
This way, I get the updates from DB and can save the Profile object but I cannot modify attributes.
For example:
mutableLiveData.value = Profile() would work.
mutableLiveData.value.userName = "name" would call getValue() instead postValue() and wouldn't work.
Did anyone find a solution for this?
Call me crazy but AFAIK there is zero reason to use a MutableLiveData for the object that you received from the DAO.
The idea is that you can expose an object via LiveData<List<T>>
#Dao
public interface ProfileDao {
#Query("SELECT * FROM PROFILE")
LiveData<List<Profile>> getProfiles();
}
Now you can observe them:
profilesLiveData.observe(this, (profiles) -> {
if(profiles == null) return;
// you now have access to profiles, can even save them to the side and stuff
this.profiles = profiles;
});
So if you want to make this live data "emit a new data and modify it", then you need to insert the profile into the database. The write will re-evaluate this query and it will be emitted once the new profile value is written to db.
dao.insert(profile); // this will make LiveData emit again
So there is no reason to use getValue/setValue, just write to your db.
If you really need to, then you can use the mediator trick.
In your ViewModel
val sourceProduct: LiveData<Product>() = repository.productFromDao()
val product = MutableLiveData<Product>()
val mediator = MediatorLiveData<Unit>()
init {
mediator.addSource(sourceProduct, { product.value = it })
}
In fragment/activity
observe(mediator, {})
observe(product, { /* handle product */ })
A Kotlin extension to trasform LiveData into MutableLiveData:
/**
* Transforms a [LiveData] into [MutableLiveData]
*
* #param T type
* #return [MutableLiveData] emitting the same values
*/
fun <T> LiveData<T>.toMutableLiveData(): MutableLiveData<T> {
val mediatorLiveData = MediatorLiveData<T>()
mediatorLiveData.addSource(this) {
mediatorLiveData.value = it
}
return mediatorLiveData
}
Since Room doesn't support MutableLiveData and has support for LiveData only, your approach of creating a wrapper is the best approach I can think of. It will be complicated for Google to support MutableLiveDatasince the setValue and postValue methods are public. Where as for LiveData they are protected which gives more control.
In your repository you can get LiveData and transform it to MutableLivedata:
var data= dao.getAsLiveData()
return MutableLiveData<T>(data.value)

Passing arguments to AndroidViewModel

I'm following the google tutorial for Room persistence but i'm stuck, right now I have the tutorial all working fine but I need to expand it and be able to pass parameters to the ViewModel because what I need is to be able to submit different queries to the repo, and maybe i'm wrong but right now i'm doing it in the ViewModel which should be able to read his field and choose the right method to talk with the repo.
WordViewModel:
public class WordViewModel extends AndroidViewModel {
private WordRepository mRepository;
private LiveData<List<Word>> mAllWords;
public int mode = 0;
public WordViewModel (Application application) {
super(application);
mRepository = new WordRepository(application);
if (mode==0)
mAllWords = mRepository.getAllWords();
else
mAllWords = mRepository.getSomethingElse();
}
LiveData<List<Word>> getAllWords() { return mAllWords; }
public void insert(Word word) { mRepository.insert(word); }
}
Then in the activity the triggers the model view we got this
mWordViewModel = ViewModelProviders.of(this).get(WordViewModel.class);
mWordViewModel.mode=1; //MY ADDITION, not working
...
mWordViewModel.getAllWords().observe(this, new Observer<List<Word>>() {
#Override
public void onChanged(#Nullable final List<Word> words) {
// Update the cached copy of the words in the adapter.
adapter.setWords(words);
}
});
...
Now the problem is that the field access and edit (the "mode" field) i've made is not working, it's like the field is getting resetted when the ViewModel is actually called and so it's always 0. What am i Missing? What is the easiest workaround considering that mode is just for explaining and eventually i'll need a lot of parameters (so creating various ViewModel is not an option)
I think you're running in to issues related to lifecycle of ViewModel itself and different variables etc you're using. I'd recommend using something like MediatorLiveData for what you're trying to do...for example (this is in Kotlin btw as that's what I'm using for similar logic I have)
class WordViewModel : ViewModel() {
.....
val mode: MutableLiveData<Int> = MutableLiveData()
val mAllWords = MediatorLiveData<List<Word>>().apply {
this.addSource(mode) {
if (mode.value == 0)
this.value = mRepository.getAllWords()
else
this.value = mRepository.getSomethingElse()
}
}
init {
mode.value = 0
}
fun setMode(m: Int) {
mode.value = m
}
}
The code where I'm doing this here is https://github.com/joreilly/galway-bus-android/blob/master/base/src/main/java/com/surrus/galwaybus/ui/viewmodel/BusStopsViewModel.kt

Android Architecture Components LiveData

I'm trying to implement a simple App using Architecture Components.
I can get the info from RestApi services using Retrofit2.
I can show the info in the respective Recyclerview and when I rotate the phone everything works as it should.
Now I want to filter by a new kind of object (by string)
Can someone guide me a little with the ViewModel, I don't know what is the best practice to do that...
I'm using MVVM...
This is my ViewModel:
public class ListItemViewModel extends ViewModel {
private MediatorLiveData<ItemList> mList;
private MeliRepository meliRepository;
/* Empty Contructor.
* To have a ViewModel class with non-empty constructor,
* I have to create a Factory class which would create instance of you ViewModel and
* that Factory class has to implement ViewModelProvider.Factory interface.
*/
public ListItemViewModel(){
meliRepository = new MeliRepository();
}
public LiveData<ItemList> getItemList(String query){
if(mList == null){
mList = new MediatorLiveData<>();
LoadItems(query);
}
}
private void LoadItems(String query){
String queryToSearch = TextUtils.isEmpty(query) ? "IPOD" : query;
mList.addSource(
meliRepository.getItemsByQuery(queryToSearch),
list -> mList.setValue(list)
);
}
}
UPDATE
I resolved this using transformation a package from lifecycle library...
enter link description here
public class ListItemViewModel extends ViewModel {
private final MutableLiveData<String> mQuery = new MutableLiveData<>();
private MeliRepository meliRepository;
private LiveData<ItemList> mList = Transformations.switchMap(mQuery, text -> {
return meliRepository.getItemsByQuery(text);
});
public ListItemViewModel(MeliRepository repository){
meliRepository = repository;
}
public LiveData<ItemList> getItemList(String query){
return mList;
}
}
#John this is my solution. I'm using lifecycle library and the solution was easier than I thought. Thx!
I'm more familiar with doing this in Kotlin but you should be able to translate this to Java easily enough (or perhaps now is a good time to start using Kotlin :) )....adapting similar pattern I have here I believe you'd do something like:
val query: MutableLiveData<String> = MutableLiveData()
val mList = MediatorLiveData<List<ItemList>>().apply {
this.addSource(query) {
this.value = meliRepository.getItemsByQuery(query)
}
}
fun setQuery(q: String) {
query.value = q
}
I'm using this pattern in following https://github.com/joreilly/galway-bus-android/blob/master/app/src/main/java/com/surrus/galwaybus/ui/viewmodel/BusStopsViewModel.kt

Categories

Resources