Modify LiveData item fetched from Room - android

I have simple RecyclerView, which I fill with LiveData.
Activity:
viewModel.getBooks().observe(this, books -> booksAdapter.setBooks(books));
ViewModel:
public LiveData<List<Book>> getBooks() {
books = booksRepository.getBooks();
return books;
}
BooksRepository just calls this DAO's method:
#Query("SELECT * FROM books")
LiveData<List<Book>> getBooks();
Everything works as expected. However, in RecyclerView, I have LIKE button for every item, and I want to create onClick listener, which changes book's flag FAVOURITE accordingly. However, Room does not support that with LiveData, and it does not support MutableLiveData as well.
One (not good) solution would be to send Application to adapter, where I would create repository and update Book entity "manually". But I know it is not a good idea to send Application to adapter.
Any idea how to make this? I just need to set one simple boolean column in Book entity. Thanks.

Updating a value within a livedata set will not update the underlying source. I would recommend updating your DAO to where when the user clicks the LIKE button, it updates the DB..which then in turn post an update to your livedata and the changes would be reflected on the view.
eg:
#Query("UPDATE books SET FAVOURITE = :value WHERE books.bookId = :bookId")
public abstract void setBookFavourited(boolean value, int bookId);
Also to note, you shouldnt need to pass Application to the adapter. Setup an interface within the adapter that tells the activity that a user pressed the favorite button.
In adapter:
public interface BookFavoriteListener {
void onBookFavorited(Book book);
}
Implement the interface in your activity, then pass 'this' into the constructor of the adapter to set the listener.

Related

How to update a row in room? (please read first)

So I observe my room db in recyclerview via viewmodel. To update a row I call update method on repository object in activity. Am I doing it right? Because this is what I have seen in tutorials. My question is if we are using repository object for update, create and viewmodel to read data, whats the use case for setdata and postdata methods of livedata? Also how to update an entire table(overwrite)?
Observing
noteViewModel.getAllNotes().observe(this, Observer<List<Note>> { notes ->
notes?.let {
notesList = notes as ArrayList<Note>
notesAdapter = NotesAdapter(notes, this#MainActivity)
recyclerView.adapter = notesAdapter
notesAdapter!!.notifyDataSetChanged()
}
})
Updating
NoteDatabase.getInstance(this#MainActivity).noteDao().updateNote(
notechecked.also { it.done = value }
Your code is fine. This is exactly how your'e supposed to observe and update rows in Room.
The use case for setData and postData methods of LiveData are.. well.. to update the LiveData object.
For example, Room uses postData() to update the LiveData object it initially returned (when you called getAllNotes()), whenever you update your DB in a way that affects your LiveData object.
Because Room calls postData() for you, you don't need to.
But, if you were to hold some value directly in your viewmodel (rather than in Room), inside a LiveData object, and you'd want to change it when the user clicks something in your view, you'd have to call setData/postData yourself.

Retrieve single record with Room to populate an Edit dialog (activity or fragment)

I have a database table which stores some records. I have been able to correctly populate a RecyclerView in a Fragment, following tutorials like this one and similar ones found via search engine.
What I want to do next is to tie an "Edit record {id}" fragment that is tied to the RecyclerView. In other words, if I click on an item in the Recycler view, another fragment(or activity) should open, load the data for record[id] from the database and then allow me to save and update the record if needed.
The point where I am stuck is retrieving the single record from the database, because I systematically end up with either (1) calling the query inside the main thread, which Room prevents me from doing, or (2) getting some random null pointer.
I have seen solutions even here on stackoverflow, but I can't make sense on how to integrate them in my case.
What I can't make sense of is how to make the async call (whether with threads/coroutines), store the result in a variable, and use it to populate the fields in the Edit fragment.
Internet search have been very disappointing, for all I find are (duplicate) tutorials that are either incomplete, irrelevant or obsolete.
Good pointers are welcome. I would prefer not to use third party libraries to do this, unless someone can explain to me the advantages in doing so.
Sorry for the long post: I haven't added code because there would be too many pieces to show and you would probably know anyway. I will answer any questions however to help out.
Also, I am new to Kotlin/Android, and I am trying to tame this beast :-)
Its hard to say anything specific without any code, but the correct way to do it would be
Retrieve all records from Room
Load them in your recycler view, so recycler adapter will have a list of all your records
setup on click listener in your recycler adapter to open the next activity or fragment
pass the primary key (as in room) of clicked item to the next activity or fragment
In your next activity retrieve a record from room using the primary key
bind the retrieved record to UI
If your recycler view and adapter are correctly setup then you should have following in your adapter
override fun onBindViewHolder(holder: YourViewHolder, position: Int) {
// dataList contains all your records as retrieved from room
// and loaded in your recycler view
setListeners(dataList[position], holder)
holder.bind(dataList[position])
}
private fun setListeners(selectedRecord: YourRecordTypeInRoom, viewHolder: YourViewHolder){
viewHolder.itemView.setOnClickListener {
var intent = Intent(viewHolder.itemView.context, NextActivity::class.java)
// pass primary key to next activity
intent.putExtra("primaryKey", selectedRecord.primaryKey)
viewHolder.itemView.context.startActivity(intent)
}
}
Now to retrieve your single record you should have something as follows in your dao
#Query("Select * FROM your_table where primaryKey = :primaryKey")
fun findByPrimaryKey(primaryKey: PrimaryKeyType): YourRecordType
Edit:
You can also modify the return type of above function to be a LiveData object, which will allow you to observe it in your activity in an async manner. with live data your code would look some thing as follows.
In Dao
#Query("Select * FROM your_table where primaryKey = :primaryKey")
fun findByPrimaryKey(primaryKey: PrimaryKeyType): LiveData<YourRecordType>
In your view model
fun getRecordByPrimaryKey(primaryKey: PrimaryKeyType) = yourDao.findByPrimaryKey(primaryKey)
and in your activity or fragment
viewModel.getRecordByPrimaryKey(primaryKey).observe(this, Observer{
// Bind your record on UI
})
1) Return fun someFunction():LiveData<Model> in Room class, (you should be able to call it from Main thread). After getting value once, you can stop observing, since you want only single value
2) You can use Kotlin Coroutines, this way you return suspend fun someFunction():Model. You can only call this function from another Coroutine, so it will be something like:
class ViewModel{
fun normalFunction(){
viewModelScope.launch{
val result = room.someFunction()
// tell View that you have result (View observes result using LiveData)
}
}
}

Best practice to save changes of a RecyclerView list item when using MVVM and LiveData?

I followed this guide to understand how to use LiveData and data binding to update the UI when changes occur in the database and also save data back to the database when I make changes in the app.
The part I'm missing is how to properly extend the things explained in the guide to a Layoutwith a RecyclerView. In this case, the ViewModel would contain a LiveData<List<Model>> models member and I would have to bind a RecyclerView list item to a Model, by adding something like this in my list item <layout/>:
<data>
<variable name="model"
type="com.example.models.Model" />
</data>
but still observe the models colletion of my ViewModel in my Activity:
viewModel.models.observe(this, Observer { models ->
modelsAdapter.data = models?.data
modelsAdapter.notifyDataSetChanged()
})
The Observer's method above will not be triggered if a property of a Model instance in the models collection changes so If I want to run some code (like saving data to my DB) when a change is made by the user on a list item which is bound to my Model, I could just add a method to my Model class and bind the onChange event of a View in my list item <layout/> to that method. Let's say that I'd have an EditText on my list item <layout/>, then I could add the lines below to run the the listItemTextChanged() method on the bound Model when the text on my EditText changes:
<EditText
...
android:text="#={model.ListItemText}"
android:onTextChanged="#{ () -> model.listItemTextChanged()}" />
However, this means that beside adding a method to my Model class I'd also have to add a reference to my Repository if I want to save the text of my EditText to the database and it feels like this is not the correct way to do it and that my ViewModel, which contains the LiveData<List<Model>> models should be responsible for communicating with my Repository to perform database operations.
I know that I could add the ViewModel as a <layout/> variable to my list item <layout/> and add a listItemTextChanged(Model model) method on my ViewModel that could then update the model in the database but that doesn't sound right either.
Could anyone point me in the right direction?
i dont use databinding however your quite right that only the view model should talk to the repository, but if your using live data then you need to observe it in your fragment /activity and that should update your recycler view
myViewModel = ViewModelProviders.of(getActivity()).get(MyViewModel.class);
myViewModel.getCards().observe(getViewLifecycleOwner(), new
Observer<List<Card>>() {
#Override
public void onChanged(#Nullable List<Card> cards) {
if (cards != null && cardAdapter != null){
cardAdapter.refreshList(cards);
}
}
}
});
so you could add a text watcher on the card and have that call a method in your view model to save it although that would probably update too much, maybe have a save button

Passing value to LiveData in Android

I'm new in development and I'm aware that stack isn't for 'full code requests'. But I'm stuck and can't find the solution.
I'm using Room database which has two columns - firstName and lastName. I'm loading the database with passed firstName parameter:
#Query("SELECT * FROM passenger WHERE firstName LIKE :firstName")
List<Passenger>getAllByName (String firstName);
It works as supposed.
But.. When I want to update Passenger, I need to populate data again, again, and again. There comes LiveData and observer.
But.. setValue in LiveData is private and I cannot send any parameters for Query line. There comes MutableLiveData, but how can I implement that?
#Eduardas Seems like you need to return LiveData instead:
LiveData<List<Passenger>> getAllByName (String name);
And you can write a transformation in the ViewModel or you can directly observe it from your Activity/Fragment.
In activity/fragment onCreate() or onResume():
YourDao.getAllByName(name).observe(this, new LiveData<List<Passenger>>(){
#Override
public void onChanged( #Nullable List<Passenger>) {
// update your adapter if the list isn't null
}
});
Something similar to above. You can add customization as per your use case.

What is good implementation on updating items that shift around a lot in RecyclerView (swapping), and immediate user feedback?

Goal(s):
1: Effortless updating for dynamic items.
Example:
I have a List<T> returned from an API, I use that list in my RecyclerView.Adapter. User swipes to refresh and a new list is returned from the API containing some new items and some updated old items. Now the older list needs to remove duplicate items.
Note: assume all items have an updated attribute that might change if a user interacts with it.
2: Immediate user feedback (this might tie in with goal #1).
Example:
To insert a new item into the RecyclerView.Adapter it needs to be created in an API first. Implementation creates object in the RecyclerView.Adapter and in the API simultaneously. When the new object is returned from the API the immediate object that was previously injected right away into the RecyclerView.Adapter "syncs" with the API response. This way the user sees immediate feedback.
Code Example:
I don't really have anything in mind for Goal #1 BUT for Goal 2 I was thinking something like this maybe inside my ViewHolder? (I have heard that updating / syncing models in Viewholders is not a good practice in general because viewholders recycle):
// JAVA 7
private void createNewObjectToBeInsertedIntoRecyclerView(String data) {
// Pass callback to API and at the same time insert object into adapter
mAdapter.addNewObject(data);
mPresenter.createObject(new SyncRequestCallback() {
#Override
public void onSuccessFromAPI(ModelObject model) {
mAdapter.updateObject(model);
}
});
}
// JAVA 8
private void createNewObjectToBeInsertedIntoRecyclerView(String data) {
// Pass callback to API and at the same time insert object into adapter
mAdapter.addNewObject(data);
mPresenter.createObject((sync) -> { mAdapter.updateObject(model); });
}
This is just off the top of my head and it is definitely bug prone.
How Can I Achieve This?:
Looking for a robust solution here, but something that doesn't involve content providers (if possible).
You should not do anything like that in the ViewHolder, just bind the data you got from the API to the UI.
What you should do is operate on the Adapter
when the new List<T> returns from the API, just make the old list in the adapter to point to this new one (oldList = newList) and call mAdapter.notifyDataSetChanged()
You can do like point 1) but that way updates the whole Adapter. If you know where in the Adapter you have inserted that item (and I assume you know), just call mAdapter.notifyItemInserted(position) or alternatively, if you have already created it the Adapter, call mAdapter.notifyItemChanged(position)

Categories

Resources