Notify Observer when item is added to List of LiveData
Asked Answered
A

10

126

I need to get an Observer event when the item is added to the List of LiveData. But as far as I understand the event receives only when I replace the old list with a new one. For example when I do the next:

list.value = mutableListOf(IssuePost(UserEntity(name, email, photoUrl), issueEntity))

Observer gets event. But when I just add item to value, Observer is silent. Could you please give me advice on how I can implement what I need?

Annalee answered 22/12, 2017 at 12:25 Comment(5)
Its wotking as You say. You can create subclass of LiveData and method "add" which would create new list from old list and append new element.Almedaalmeeta
@Almedaalmeeta What do you mean? Is it work when I add an item to the list?Annalee
I mean as it's working as "But as fa as I understand event receives only when I replace the old list with new one"Almedaalmeeta
@Almedaalmeeta Yes I can, but it is wasteful. Is there any way of Android SDK to do this?Annalee
If you want use rx version https://mcmap.net/q/176070/-how-can-i-create-an-observer-over-a-dynamic-list-in-rxjavaCoquille
W
118

Internally, LiveData keeps track of each change as a version number (simple counter stored as an int). Calling setValue() increments this version and updates any observers with the new data (only if the observer's version number is less than the LiveData's version).

It appears the only way to start this process is by calling setValue() or postValue(). The side-effect is if the LiveData's underlying data structure has changed (such as adding an element to a Collection), nothing will happen to communicate this to the observers.

Thus, you will have to call setValue() after adding an item to your list. I have provided two ways you could approach this below.

Option 1

Keep the list outside of the LiveData and update with the reference any time the list contents change.

private val mIssuePosts = ArrayList<IssuePost>()
private val mIssuePostLiveData = MutableLiveData<List<IssuePost>>()

fun addIssuePost(issuePost: IssuePost) {
   mIssuePosts.add(issuePost)
   mIssuePostLiveData.value = mIssuePosts
}

Option 2

Keep track of the list via the LiveData and update the LiveData with its own value whenever the list contents change.

private val mIssuePostLiveData = MutableLiveData<MutableList<IssuePost>>()

init {
   mIssuePostLiveData.value = ArrayList()
}

fun addIssuePost(issuePost: IssuePost) {
    mIssuePostLiveData.value?.add(issuePost)
    mIssuePostLiveData.value = mIssuePostLiveData.value
}

Either of these solutions should help you from having to create a new list every time you modify the current list just to notify the observers.

UPDATE:

I've been using similar techniques for a while now as Gnzlt has mentioned in his answer to use a Kotlin extension function to assign the LiveData to itself to simplify the code. This is essentially Option 2 automated :) I would recommend doing that.

Wrist answered 28/2, 2018 at 5:43 Comment(1)
Am I correct that in this case observer will receive new list each time? Is it possible to subscribe only to new element being added to the list?Caracara
U
115

I use a Kotlin Extension Function to make it easier:

fun <T> MutableLiveData<T>.notifyObserver() {
    this.value = this.value
}

Then use it in any MutableLiveData like this:

fun addIssuePost(issuePost: IssuePost) {
    mIssuePostLiveData.value?.add(issuePost)
    mIssuePostLiveData.notifyObserver()
}
Unbodied answered 29/8, 2018 at 10:29 Comment(6)
Better way fun <T> MutableLiveData<List<T>>.add(item: T) { val updatedItems = this.value as ArrayList updatedItems.add(item) this.value = updatedItems }Scutate
@musooff, In My case, this caused crash. That's why, I Changed a little in your code. I used MutableLiveData<MutableList<T>> instead of MutableLiveData<List<T>>Drifter
Gnzlt's original solution is best. It is more universal since it separates the data type from the notification (separation of concerns).Alceste
If you use notifyObserver() from background thread the app will crash. Use this.postValue(this.value) if you notify from a non UI thread.Neophyte
+1 @toshkinl I ran into the same issue, using postValue instead of this.value = this.value resolved the issue.Denning
Can anyone confirm if it is still working? It is not working for me.Beamer
C
13

Late to the party but here is a more concise version of Gnzlt's answer with null-check and for both mutable and immutable lists:

// for mutable list
operator fun <T> MutableLiveData<MutableList<T>>.plusAssign(item: T) {
    val value = this.value ?: mutableListOf()
    value.add(item)
    this.value = value
}

// for immutable list
operator fun <T> MutableLiveData<List<T>>.plusAssign(item: T) {
    val value = this.value ?: emptyList()
    this.value = value + listOf(item)
}

And in your code:

list += IssuePost(UserEntity(name, email, photoUrl), issueEntity))
Comeback answered 14/9, 2019 at 15:42 Comment(1)
How would you use this generically?Infective
H
11

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 follows:

fun <T> MutableLiveData<MutableList<T>>.addNewItem(item: T) {
    val oldValue = this.value ?: mutableListOf()
    oldValue.add(item)
    this.value = oldValue
}

fun <T> MutableLiveData<MutableList<T>>.addNewItemAt(index: Int, item: T) {
    val oldValue = this.value ?: mutableListOf()
    oldValue.add(index, item)
    this.value = oldValue
}

fun <T> MutableLiveData<MutableList<T>>.removeItemAt(index: Int) {
    if (!this.value.isNullOrEmpty()) {
        val oldValue = this.value
        oldValue?.removeAt(index)
        this.value = oldValue
    } else {
        this.value = mutableListOf()
    }
}

Then add/remove items from your MutableLiveData like:

// Here is your IssuePost list
var issuePostList = MutableLiveData<MutableList<IssuePost>>()

// Add new item to your list
issuePostList.addNewItem(IssuePost(UserEntity(name, email, photoUrl), issueEntity))

// Delete an item from your list at position i
issuePostList.removeItemAt(i)

// Add new item to your list at position i
issuePostList.addNewItemAt(i, IssuePost(UserEntity(name, email, photoUrl), issueEntity))
Huntress answered 16/5, 2020 at 10:20 Comment(5)
Good point * only when reference is changed *Branscum
It isn't clear how this causes the observer to get notified each time a new value is added. The reference to the list (except for the first time) is always the same.Butadiene
Is there anyway If we wanted to expose LiveData<List> (immutable list) from our viewModel object?Loveinidleness
@AndroidDev Actually the observer only gets notified whenever a new value is assigned to the LiveData variable.Huntress
@EhsanShadi There are many ways to get the value of a LiveData' from ViewModel` 1. You can observe that LiveData, 2. You can write a function in the ViewModel or in your Activity that returns the value of that LiveData, 3. You can emit the value of that LiveData by using another LiveData. If you are not clear yet! then please ask a question describing your problem and mention me there in the comment section, I will be there for you to help.Huntress
G
8

I found a better solution for Kotlin:

class MutableListLiveData<T>(
        private val list: MutableList<T> = mutableListOf()
) : MutableList<T> by list, LiveData<List<T>>() {

    override fun add(element: T): Boolean =
            element.actionAndUpdate { list.add(it) }

    override fun add(index: Int, element: T) =
            list.add(index, element).also { updateValue() }

    override fun addAll(elements: Collection<T>): Boolean =
            elements.actionAndUpdate { list.addAll(elements) }

    override fun addAll(index: Int, elements: Collection<T>): Boolean =
            elements.actionAndUpdate { list.addAll(index, it) }

    override fun remove(element: T): Boolean =
            element.actionAndUpdate { list.remove(it) }

    override fun removeAt(index: Int): T =
            list.removeAt(index).also { updateValue() }

    override fun removeAll(elements: Collection<T>): Boolean =
            elements.actionAndUpdate { list.removeAll(it) }

    override fun retainAll(elements: Collection<T>): Boolean =
            elements.actionAndUpdate { list.retainAll(it) }

    override fun clear() =
            list.clear().also { updateValue() }

    override fun set(index: Int, element: T): T =
            list.set(index, element).also { updateValue() }

    private fun <T> T.actionAndUpdate(action: (item: T) -> Boolean): Boolean =
            action(this).applyIfTrue { updateValue() }

    private fun Boolean.applyIfTrue(action: () -> Unit): Boolean {
        takeIf { it }?.run { action() }
        return this
    }

    private fun updateValue() {
        value = list
    }
}

The advantage of this implementation is that you can use Kotlin extension functions on MutableListLiveData. Then you can use it like this, and your LiveData automatically updates:

private val _items = MutableListLiveData<Item>()
val items: LiveData<List<Item>> = _items

fun addItem(item: Item) {
   _items.add(item)
}
Gradient answered 21/1, 2021 at 14:37 Comment(1)
How to use this in java ?Acromion
A
4

How about this?

public class ListLiveData<T> extends LiveData<List<T>> {
    public void addAll(List<T> items) {
        if (getValue() != null && items != null) {
            getValue().addAll(items);
            setValue(getValue());
        }
    }

    public void clear() {
        if (getValue() != null) {
            getValue().clear();
            setValue(getValue());
        }
    }

    @Override public void setValue(List<T> value) {
        super.setValue(value);
    }

    @Nullable @Override public List<T> getValue() {
        return super.getValue();
    }
}

// add changed listener
    mMessageList.observe(mActivity, new Observer() {
        @Override public void onChanged(@Nullable Object o) {
            notifyDataSetChanged();
        }
    });
Andre answered 30/11, 2018 at 7:59 Comment(0)
A
3

I ran into the same problem and decided that adding "value = value" or a call to another method everywhere was too much of a pain and very error-prone.

So, I created my own classes that wrap the collections. Observe will now observe the contents of the collection instead of the collection itself.

The code is available on GitHub at: ObservableCollections

This is a FIRST version so please excuse any problems. It is not yet available for gradle include so you will have to download it and add it as a library module to you application.

At present it includes the following collections:

ArrayDeque
ArrayList
LinkedList
Queue
Stack
Vector

More will be added as time allows. Feel free to request specific ones.

And PLEASE report any problems.

Edit: It now includes far more collections. See there for details.
Also, it is available for include via gradle. Again, see there for details.

Adduction answered 10/4, 2019 at 14:23 Comment(0)
P
2

I came up with my own solution (Java), I created a custom MutableLiveData that keeps note of the last time the list was modified and notifies us accordingly:

public class MutableListLiveData<T> extends MutableLiveData<List<T>> {
    private final MutableLiveData<Long> lastModified = new MutableLiveData<>();
    private List<T> items;
    private ListObserver<List<T>> callback;

    public MutableListLiveData() {
        this.items = new ArrayList<>();
    }

    public void addItem(T item) {
        items.add(item);
        onListModified();
    }

    public void removeItem(int position) {
        items.remove(position);
        onListModified();
    }

    public void updateItem(int position, T item) {
        items.set(position, item);
        onListModified();
    }

    public T getItem(int position) {
        return items.get(position);
    }

    private void onListModified() {
        lastModified.setValue(System.currentTimeMillis());
    }

    @Override
    public List<T> getValue() {
        return items;
    }

    @Override
    public void setValue(List<T> items) {
        this.items = items;
        onListModified();
    }

    public void observe(@NonNull LifecycleOwner owner, ListObserver<List<T>> callback) {
        this.callback = callback;
        lastModified.observe(owner, this::onListItemsChanged);
    }

    private void onListItemsChanged(long time) {
        if (callback != null) callback.onListItemsChanged(items, items.size());
    }

    public interface ListObserver<T> {
        void onListItemsChanged(T items, int size);
    }
}

Usage:

MutableListLiveData<List<Integer>> myList = new MutableListLiveData<>();
myList.observe(owner, this::onListChanged(items, size);

private void onListChanged(List<Integer> items, int size) {
    // Do Something
}
Pyroligneous answered 26/8, 2021 at 23:1 Comment(0)
H
0

Inspired by @user3682351 here is my solution. It seems that we cannot update properties of a LiveData value individually so the value must be updated on each operation. This is essentially a small wrapper on live data with convenience methods for modifying properties of a HashMap

import androidx.lifecycle.LiveData

/**
 * Hash Map Live Data
 *
 * Some convenience methods around live data for HashMaps. Putting a value on this will also update the entire live data
 * as well
 */
class HashMapLiveData<K, V> : LiveData<HashMap<K, V>>() {

    /**
     * Put a new value into this HashMap and update the value of this live data
     * @param k the key
     * @param v the value
     */
    fun put(k: K, v: V) {
        val oldData = value
        value = if (oldData == null) {
            hashMapOf(k to v)
        } else {
            oldData.put(k, v)
            oldData
        }
    }

    /**
     * Add the contents to the HashMap if there is one existing, otherwise set the value to this HashMap and update the
     * value of this live data
     * @param newData the HashMap of values to add
     */
    fun putAll(newData: HashMap<K, V>) {
        val oldData = value
        value = if (oldData != null) {
            oldData.putAll(newData)
            oldData
        } else {
            newData
        }
    }

    /**
     * Remove a key value pair from this HashMap and update the value of this live data
     * @param key the key to remove
     */
    fun remove(key: K) {
        val oldData = value
        if (oldData != null) {
            oldData.remove(key)
            value = oldData
        }
    }

    /**
     * Clear all data from the backing HashMap and update the value of this live data
     */
    fun clear() {
        val oldData = value
        if (oldData != null) {
            oldData.clear()
            value = oldData
        }
    }

    var value: HashMap<K, V>?
        set(value) = super.setValue(value)
        get() = super.getValue()

}

Headwaters answered 19/2, 2019 at 16:11 Comment(0)
M
0

I think this class will help you:

class ArrayListLiveData<T> : MutableLiveData<ArrayList<T>>()
{
    private val mArrayList = ArrayList<T>()

    init
    {
        set(mArrayList)
    }

    fun add(value: T)
    {
        mArrayList.add(value)
        notifyChanged()
    }

    fun add(index: Int, value: T)
    {
        mArrayList.add(index, value)
        notifyChanged()
    }

    fun addAll(value: ArrayList<T>)
    {
        mArrayList.addAll(value)
        notifyChanged()
    }

    fun setItemAt(index: Int, value: T)
    {
        mArrayList[index] = value
        notifyChanged()
    }

    fun getItemAt(index: Int): T
    {
        return mArrayList[index]
    }

    fun indexOf(value: T): Int
    {
        return mArrayList.indexOf(value)
    }

    fun remove(value: T)
    {
        mArrayList.remove(value)
        notifyChanged()
    }

    fun removeAt(index: Int)
    {
        mArrayList.removeAt(index)
        notifyChanged()
    }

    fun clear()
    {
        mArrayList.clear()
        notifyChanged()
    }

    fun size(): Int
    {
        return mArrayList.size
    }
}

and this extensions:

fun <T> MutableLiveData<T>.set(value: T)
    {
        if(AppUtils.isOnMainThread())
        {
            setValue(value)
        }
        else
        {
            postValue(value)
        }
    }

    fun <T> MutableLiveData<T>.get() : T
    {
        return value!!
    }

    fun <T> MutableLiveData<T>.notifyChanged()
    {
        set(get())
    }
Morphology answered 8/7, 2020 at 0:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.