How can I add new Items to a MutableLiveData List? I want to build an infinite scrolling Recyclerview. So I have a List with 10 items and after clicking a "load more button" I want to add ten new Items.
This are my lists of products:
private companion object {
private var _products = MutableLiveData<MutableList<Product>>()
}
val products: LiveData<MutableList<Product>> = _products
Ant this is my loading function:
fun loadProducts(category: Category) { // category: String
_isViewLoading.postValue(true)
AppService.fetchProducts(category, neuheitenCounter) { success, response ->
_isViewLoading.postValue(false)
if(success) {
when(category) {
Category.NEWS -> {
if(_products.value == null) {
_products.value = response?.products //as List<Product>
} else {
response?.let {
_products.value?.addAll(response?.products)
}
}
neuheitenCounter++
}
}
}
}
}
If I call _products.value = response?.products it fires the observe method in my Activity. But if I call addAll or add, the observer method is not called.
Actually, it's how you should update the adapter
Add new items to the existing list
Notify LiveData by assigning the same list to its value. Here is the same question with the solution.
When your Activity is notified that the list is changed, it should notify the adapter about the changes by calling its notifyDataSetChanged() method.
When you call notifyDataSetChanged() method of the adapter, it compares previous items with the new ones, figures out which of them are changed and updates the RecyclerView. You can help the adapter by calling notifyItemRangeInserted() instead of notifyDataSetChanged(). notifyItemRangeInserted() accepts a range on inserted items, in your case you can calculate it but taking the difference between adapter.itemCount and the productList.size.
MutableLiveData.postValue(T value)
I had that same issue .I copied data of LiveData in new List then i added new data to new List.And assigned new List to LiveData.I know this not exact SOLUTION but this solved problem.
Try this.
var tempList= _products.value
tempList.addAll(response?.products)
_products.value = tempList
I hope this will help you.
Related
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.
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())
I have the following method -
private fun initRoomObserving() {
dashboardViewModel = ViewModelProvider(this).get(DashboardViewModel::class.java)
dashboardViewModel.getAllMessagesEntities().observe(this, Observer { receivedMessageList ->
receivedMessageList.forEach {
if (!userPhoneNumber.equals(it.senderUsername)) {
it.isReceiver = true
}
if (!messagesList.contains(it)) {
messagesList.add(it)
}
}
conversationAdapter.notifyItemInserted(messagesList.size)
conversationAdapter.notifyItemRangeChanged(messagesList.size - 1,messagesList.size)
})
}
For some reason the entire list is being rendered again for each time a new entity is beining added, even though I am explicitly notifyItemInserted and not notifyDataSetChanged
Why is this happening and what am I missing?
The seconds parameter for notifyItemRangeChanged takes the count as value so since the value of changes item is always one so pass 1 instead of messagesList.size as:
conversationAdapter.notifyItemRangeChanged(messagesList.size - 1, 1)
Additionally, someCount can be variable which can track the number of changed items which can be used for notifyItemRangeChanged when you will have more then one item for updates.
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.
final FirebaseRecyclerAdapter adapter = new FirebaseRecyclerAdapter(Service.class, R.layout.browse_service_detail, ServiceHolder.class, mReference){
#Override
protected void populateViewHolder(ServiceHolder serviceHolder, Service service, int position) {
serviceHolder.setServiceName(service.getName());
serviceHolder.setInfo("От " + service.getPrice1());
service.setQuantitySelected(service.getQuantityEnabled());
if (Order.getInstance().getServices() != null) {
for (Service serviceFromSingleton : Order.getInstance().getServices()) {
if (serviceFromSingleton.getName() == serviceHolder.getServiceName().getText().toString()) {
serviceHolder.getServiceName().setSelected(true);
serviceHolder.getServiceName().setTextColor(getResources().getColor(R.color.yellow));
}
}
}
//add item to array
servicesList.add(service);
}
}
};
When I run this activity, it records the visible list objects to an array, but when I scroll down and go back up, it duplicates the first elements again into the array. How to fix it? For an item to be added only once.
I don't think there is any issue in RecyclerAdapter..I think the list only inserting same data multiple times.
why not you check whether the list is empty or not before adding data into it and clear the data if its not empty and then add new.
if(servicesList.isEmpty())
servicesList.add(service);
//else clear and add data
else{
servicesList.clear();
servicesList.add(service);
}
To handle data duplicacy, you can use a Set which will ignore duplicate inserts on scrolling.
servicesList.add(service);
Set<Service> mSet= new HashSet<Service>();
mSet.addAll(servicesList);
servicesList.clear();
servicesList.addAll(mSet);
OR use Set other than ArrayList
little clumsy but will work for you.