How to properly update Android's RecyclerView using LiveData? - android

Bottom Line Question
If I'm using MutableLiveData<List<Article>>, is there a way to properly notify observers when the title/content of an Article has changed, a new Article has been added, and an Article has been removed?
It seems the notifications are only possible when an entirely new collection is set on the LiveData, which would seem to result in a really inefficient UI refresh.
Hypothetical Example
Suppose the following...
My LiveData class looks something like this:
public class ArticleViewModel extends ViewModel {
private final MutableLiveData<List<Article>> mArticles = new MutableLiveData<>();
}
I want to display the Articles in a list by using the RecyclerView. So any time my Fragment observes a change in the ArticleViewModel's LiveData it calls the following method on my custom RecyclerView.Adapter class:
public class ArticleRecyclerViewAdapater extends RecyclerView.Adapter<Article> {
private final ArrayList<Article> mValues = new ArrayList<>();
public void resetValues(Collection<Article> articles) {
mValues.clear();
mValues.addAll(articles);
notifyDataSetChanged();
}
}
Finally, my application will allow the user to add a new Article, delete an existing Article, and change an existing Article's name (which needs to be updated in the RecyclerView list). How can I do that properly?
Add/Remove Article
It seems the LiveData construct doesn't notify observers if you add/remove an item from the underlying Collection. It seems you'd have to call LiveData#setValue, perhaps the ArticleViewModel would need a method that looks something like this:
public void deleteArticle(int index) {
final List<Article> articles = mArticles.getValue();
articles.remove(index);
mArticles.setValue(articles);
}
Isn't that really inefficient because it would trigger a complete refresh in the RecyclerView.Adapter as opposed to just adding/removing a single row?
Change Name
It seems the LiveData construct doesn't notify observers if you change the contents of an item in the underlying collection. So if I wanted to change the title of an existing Article and have that reflected in the RecyclerView then my ArticleViewModel would have to modify the object and call LiveData#setValue with the entire collection.
Again, isn't this really inefficient because it would trigger a complete refresh in the RecyclerView.Adapter?

Case1:
When you add or delete
So when you add or delete the element in the list you don't change the refernce of list item so every time you modify the liveData item you have to update live data value by calling setValue method(if you are updating the item on main thread)or Post value(when you are updating the value on background thread)
The problem is that it is not efficient
Solution
Use diffutil
Case 2:When you are updating the item property by editing the item.
The Problem
LiveData will only alert its observers of a change if the top level value has changed. In the case of a complex object, though, that means only when the object reference has changed, but not when some property of the object has changed.
Solution
To observe the change in property you need PropertyAwareMutableLiveData
class PropertyAwareMutableLiveData<T: BaseObservable>: MutableLiveData<T>() {
private val callback = object: Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
value = value
}
}
override fun setValue(value: T?) {
super.setValue(value)
value?.addOnPropertyChangedCallback(callback)
}
}
Two things to note here:
1.First is that our value is a generic type which implements the BaseObservable interface. This gives us access to the OnPropertyChangedCallback.
2.Next is that, whenever some property of the BaseObservable changes, we simply reset the top level value property to be its current value, thus alerting the observers of the LiveData that something has changed.

LiveData will only notify when its wrapped object reference is changed. When you assign a new List to a LiveData then it will notify because its wrapped object reference is changed but if add/remove items from a LiveData's List it will not notify because it still has the same List reference as wrapped object. So you can overcome this problem by making an extension of MutableLiveData as explained here in another stackoverflow question.

I know it's too late to answer.
But I hope it will help other developers searching for a resolution on a similar issue.
Take a look at LiveAdapter.
You just need to add the latest dependency in Gradle.
dependencies {
implementation 'com.github.RaviKoradiya:LiveAdapter:1.3.4'
// kapt 'com.android.databinding:compiler:GRADLE_PLUGIN_VERSION' // this line only for Kotlin projects
}
and bind adapter with your RecyclerView
// Kotlin sample
LiveAdapter(
data = liveListOfItems,
lifecycleOwner = this#MainActivity,
variable = BR.item )
.map<Header, ItemHeaderBinding>(R.layout.item_header) {
onBind{
}
onClick{
}
areContentsTheSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
areItemSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
}
.map<Point, ItemPointBinding>(R.layout.item_point) {
onBind{
}
onClick{
}
areContentsTheSame { old: Point, new: Point ->
return#areContentsTheSame old.id == new.id
}
areItemSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
}
.into(recyclerview)
That's it. Not need to write extra code for adapter implementation, observe LiveData and notify the adapter.

Related

correct way to handle mutable state of list of data in jetpack compose

For a mutable state containing integer like this,
var data by rememberSaveable {
mutableStateOf(-1)
}
We can update data using
data = 5
And it would update the data and trigger recomposition.
Now, my requirement is to remember a list of integers.
Declared like this,
var data by rememberSaveable {
mutableStateOf(mutableListOf<Int>())
}
The list operations like clear(), add(), and remove() update the data in the list but recomposition is not triggered as the list instance is still the same.
To handle this, I replaced the following list operations with these assignments.
Instead of data.clear(), used data = arrayListOf()
Instead of data.add(ele), used data = mutableListOf(*data.toTypedArray(), ele)
Similarly, for remove() tried this,
data.remove(ele)
data = mutableListOf(*data.toTypedArray())
But this is not triggering recomposition.
What is the correct alternative for remove()?
And is there any generic to handle all list operations?
You have two options here:
Declare your list as mutableStateListOf. In this case you can do any operations on it, like clear(), add(), remove(). See this answer how you can use it with rememberSaveable, or move it to the view model.
val data = remember {
mutableStateListOf<Int>()
}
data.clear()
Sometimes it's more comfortable to use mutableStateOf, in this case you can update it value like this:
var data by rememberSaveable(Unit) {
mutableStateOf(listOf<Int>())
}
data = data.toMutableList().apply {
clear()
}.toImmutableList()

How to change visibility of a TextView if a MutableList is empty? (Android/Kotlin)

I'm working on a simple calorie counter app using two fragments and a ViewModel. I'm a beginner and this is a modification of an app I just created for a course (this app is not a homework assignment). It uses ViewModel and has a fragment that collects user input and a fragment that displays the input as a MutableList of MutableLiveData. I would like for the list screen to initially be empty except for a TextView with instructions, and I'd like the instructions to disappear once an entry has been added to the list. My class instructor told me to use an if-else statement in the fragment with the list to achieve this, but it's not working. He didn't tell me exactly where to put it. I tried a bunch of different spots but none of them worked. I don't get errors - just no change to the visibility of the TextView.
Here is the code for the ViewModel with the list:
val entryList: MutableLiveData<MutableList<Entry>>
get() = _entryList
init {
_entry = MutableLiveData<Entry>()
_entryList.value = mutableListOf()
}
fun addEntry(entryInfo: Entry){
_entry.value = entryInfo
_entryList.value?.add(_entry.value!!)
}
}
And this is the code for the observer in the list fragment:
Observer { entryList ->
val entryListView: View = inflater.inflate(R.layout.fragment_entry_list, null, false)
if (entryList.isNullOrEmpty()) {
entryListView.instructions_text_view.visibility = View.VISIBLE
} else {
entryListView.instructions_text_view.visibility = View.GONE
}
entryList.forEach {entry ->
val view: View = inflater.inflate(R.layout.entry_list_item, null, false)
view.date_entry_text_view.text = String.format(getString(R.string.date), entry.date)
view.calories_entry_text_view.text =
view.line_divider
binding.entryList.addView(view)
}
Thanks for any help.
I guess you are expecting your observer to get notified of the event when you are adding entryInfo to your event list (_entryList.value?.add(_entry.value!!).
But this won't happen as you are just adding an element to the same mutable list, and as the list reference hasn't changed, live data won't emit any update.
To solve this, you have two options.
Create a new boolean live data which controls when to show and hide the info text. Set its initial value to false, and update it to true in addEntry() function.
Instead of updating the same mutable list, create of copy of it, add the element and set the entryList.value equal to this new list. This way your observer will be notified of the new list.
Additionally, its generally not a good practice to expose mutable data unless there is no alternative. Here you are exposing a mutable list of Entry and that too in the form of a mutable live data. Ideally, your should be exposing LiveData<List<Entry>>.
This is one possible implementation of all the points that I mentioned:
private val _entryList = MutableLiveData(listOf<Entry>()) // Create a private mutable live data holding an empty entry list, to avoid the initial null value.
val entryList: LiveData<List<Entry>> = _entryList // Expose an immutable version of _entryList
fun addEntry(entryInfo: Entry) {
_entryList.value = entryList.value!! + entryInfo
}
I haven't used the _entry live data here, but you can implement it the same way.
set your viewModel to observe on entry added.
I think you have gotten your visibility toggle in the your if else blocks wrong.
if (entryList.isNullOrEmpty()) {
entryListView.instructions_text_view.visibility = View.GONE // OR View.INVISIBLE
} else {
entryListView.instructions_text_view.visibility = View.VISIBLE
}
Your Observer should get notified of changes to entryList when _entryList has changed. Make sure you are calling addEntry() function to trigger the notification.

Removing elements from a list that is used by liveData does not remove any

Android 4.1.2
Kotlin 1.4.21
I have the following live data that I add to, but when it comes to removing it doesn't remove any elements.
val selectedLiveData by lazy { MutableLiveData<List<Core>>() }
I don't want to trigger the observers so I am not assigning the value as I just want to remove a single element from the liveData list and only trigger when adding.
None of the following work
selectedLiveData.value?.toMutableList()?.apply {
removeAt(0)
}
selectedLiveData.value?.toMutableList()?.apply {
removeFirst()
}
selectedLiveData.value?.toMutableList()?.apply {
remove(Core)
}
I am adding my elements like this and then assigning the value so the observers to this live data get updated:
selectedLiveData.value = selectedLiveData.value?.toMutableList()?.apply {
add(core)
}
What you wanted is
val selectedLiveData = MutableLiveData<List<Core>>(emptyList())
Then
selectedLiveData.value = selectedLiveData.value.toMutableList().apply {
removeAt(0)
}.toList()
So what are you doing exactly:
You create a MutableLiveData with a List of objects. As we know in Kotlin List is immutable, so it's readonly.
If you want to add / remove items from a list, you should use MutableList.
If we look the documentation of toMutableList which you are using:
/**
* Returns a new [MutableList] filled with all elements of this collection.
*/
public fun <T> Collection<T>.toMutableList(): MutableList<T> {
return ArrayList(this)
}
So every time you try to remove an item via:
selectedLiveData.value?.toMutableList()
you actually perform that operation on a new MutableList not the original one.
If you want to add / remove I suggest you to use MutableList in your MutableLiveData so you can create something similar to this:
private val selectedLiveData = MutableLiveData<MutableList<Int>>()
// Init
selectedLiveData.value = mutableListOf(100, 200)
// Add items
selectedLiveData.value?.add(2)
selectedLiveData.value?.add(10)
selectedLiveData.value?.add(50)
// Remove item
selectedLiveData.value?.remove(2)
selectedLiveData.postValue(selectedLiveData.value.toMutableList().apply {
removeAt(0)
}.toList())

Updating a RecyclerView by a new LiveData<List> return from Room dynamically

I have a conventional Room->DAO->Livedata->Repositiry->ViewModel->RecyclerView app. Different buttons of UI must pass different lists of data to RecyclerView.
By button click I want:
Make new #Query in DAO and get new LiveData<`List> object in return.
Put this new data into the RecyclerViewAdapter and call notifyDataSetChanged () to make new List visuals.
The Dao #Query:
#Query("SELECT * FROM entry_table WHERE path LIKE :path ORDER BY priority DESC")
LiveData<List<Entry>> getNotesOfFolder(String path); //Returns LiveData with List of Entries
The recyclerView is updated via onChanged of Observer like this:
public class RecyclerViewActivity extends AppCompatActivity {…
Observer<List<Entry>> entryObserver = new Observer<List<Entry>>() {
#Override
public void onChanged(List<Entry> entries) {
recyclerAdapter.setEntries(entries);
}
};
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.EntryHolder> {…
public void setEntries(List<Entry> entries) {
this.entries = entries; //setting LiveData content to adapter's List (i.e. entries)
notifyDataSetChanged();
The problem is that my Observer does not call the onChange method when LiveData receives new value from DAO. I believe it is because this LiveData’s content is not CHANGED but REPLACED by another LiveData.
I tried to re-subscribe the Observer to LiveData again and it somewhat worked, but when I try to call some conventional Room queries like #Delete, I got multiple (up to 10!) onChange calls and some of them behave weirdly and pass the wrong List to RVadapter.
So there two questions:
How can I just call onChanged() of my Observer?
Is there some other stylish way of passing new LiveData object to RecyclerView dynamically?
1)
In viewModel , create a getter method for live data:
//...
private LiveData<List<Entry>> liveData;
//...
public LiveData<List<Entry>> getLiveData() {
return liveData;
}
in Activity:
#Override
protected void onCreate(Bundle savedInstanceState) {
//...
viewModel.getLiveData().observe(this, new Observer<List<Entry>>() {
#Override
public void onChanged(List<Entry> entryList) {
//set new value here
}
});
}
2) DiffUtil is very helpful to update your list in recycler view and it gives you some nice animations.
I tried to re-subscribe the Observer to LiveData again and it somewhat worked, but when I try to call some conventional Room queries like #Delete, I got multiple (up to 10!) onChange calls and some of them behave weirdly and pass the wrong List to RVadapter.
This would make sense if you didn't first unsubscribe your observer from the old LiveData object... the one you replace when your query changes.
If your query updates, you will need to get a new LiveData from the DAO. If you overwrite your old LiveData with the new one, you will not only need to (re-)subscribe your Observer to the new one, you will also need to unsubscribe it from the old one. Otherwise it will live on and keep updating the observer.

Unable to get ViewModel instance in RecyclerView.Adapter class Kotlin

I'm new to Kotlin and trying to create an alarm clock app. In this app I'm using LiveData and RecycleView. Right now I need to change the alarm status:
Here is my AlarmsRecyclerAdapter where i tried to create .onClickListener{}
override fun onBindViewHolder(holder: AlarmsRecyclerAdapter.AlarmItemHolder, position: Int) {
//mAlarmViewModel = ViewModelProviders.of( context as Fragment)[AlarmViewModel::class.java]
if (mAlarms != null) {
val current = mAlarms!!.get(position)
holder.view.edit_time_button.text = current.printTime()
holder.view.switch_alarm_enabled.isEnabled = current.enabled
holder.view.switch_alarm_enabled.setOnClickListener {
current.enabled = !current.enabled
// mAlarmViewModel.insert(current)
}
} else {
// Covers the case of data not being ready yet.
holder.view.edit_time_button.text = "no timer"
}
}
I also tried to get instance of ViewModel in the comment line, but it just throws errors like
java.lang.ClassCastException: android.app.Application cannot be cast to androidx.fragment.app.FragmentActivity
at com.xxx.alarm.AlarmsRecyclerAdapter.onBindViewHolder(AlarmsRecyclerAdapter.kt:58)
at com.xxx.alarm.AlarmsRecyclerAdapter.onBindViewHolder(AlarmsRecyclerAdapter.kt:33)
I need to change the alarms in the database, so how can I get an instance of ViewModel in the adapter class? Or is there better way to manage the data changing?
Not really sure about getting your ViewModel inside your RecyclerView, and not really sure if this would be considered best practice. But here is the way I am doing this, and have others seen doing it.
First you create your ViewModel in you Fragment.
Then you observe your AlarmData and when it changes you update the data in your RecyclerAdapter.
So in your Fragment you do something like this():
mAlarmViewModel = ViewModelProviders.of( context as Fragment)[AlarmViewModel::class.java]
mAlarmViewMode.getAlarms().observe(...
mAdapter.setData(newData)
and inside you Adapter you add the following:
setData(data:List) {
mAlarms= data;
notifyDataSetChanged();
}
this should keep your data updated.
Now for the changing of your data.
Try Setting the OnclickListener inside your ViewHolder, as this is going to increase the speed of your app.
to get your current value you could do this:
val current = mAlarms!!.get(getAdapterPosition())
Finally you shold add a Interface to your Adapter, something like this:
interface ItemSelectedListener {
fun onItemSelected(item:Any, v:View)
}
Set this interface from your Fragment and call it from the onClickListener.
Then you have all the data you need inside your Fragment and can modify it from there

Categories

Resources