1
\$\begingroup\$

I have wrote a MutableLiveList class for android, similar to existing MutableLiveData, but this class instead handles a list and notifies its observers on structural changes like adding items and removing items. It is intended to be used inside viewModels similar to MutableLiveData. Class implementation is like this:

class MutableLiveList<E> : MutableList<E> {

    private val list = mutableListOf<E>()
    private val listChange = MutableLiveData<Triple<Int, Int, ListEvent>>()

    fun observe(owner: LifecycleOwner, observer: ListObserver) {
        //newly added observer must not listen to old changeEvents
/*EDIT*/listChange.value = Triple(-1, 0, NONE)

        listChange.observe(owner, {
            when (it.third) {
/*EDIT*/        NONE -> {}
                ADD -> observer.onAdded(it.first, it.second)
                REMOVE -> observer.onRemoved(it.first, it.second)
            }
        })
    }

    //List methods : delegate to corresponding list.methods
    override val size get() = list.size
    override fun contains(element: E) = list.contains(element)
    override fun containsAll(elements: Collection<E>) = list.containsAll(elements)
    override fun get(index: Int) = list[index]
    override fun indexOf(element: E) = list.indexOf(element)
    override fun lastIndexOf(element: E) = list.lastIndexOf(element)
    override fun isEmpty() = list.isEmpty()
    override fun iterator() = list.iterator()
    override fun listIterator() = list.listIterator()
    override fun listIterator(index: Int) = list.listIterator(index)
    override fun subList(fromIndex: Int, toIndex: Int) = list.subList(fromIndex, toIndex)

    //MutableList methods : trigger list observers.
    override fun add(element: E) = add(size, element).run { true }
    override fun add(index: Int, element: E) = list.add(index, element).also {
        listChange.value = Triple(index, 1, ADD)
    }

    override fun addAll(elements: Collection<E>) = addAll(size, elements)
    override fun addAll(index: Int, elements: Collection<E>) = list.addAll(index, elements).also {
        listChange.value = Triple(index, elements.size, ADD)
    }

    override fun clear() {
        val size = size
        list.clear()
        listChange.value = Triple(0, size, REMOVE)
    }

    override fun remove(element: E): Boolean {
        val index = list.indexOf(element)
        if (index == -1)
            return false
        removeAt(index)
        return true
    }

    override fun removeAt(index: Int) = list.removeAt(index).also {
        listChange.value = Triple(index, 1, REMOVE)
    }

    override fun retainAll(elements: Collection<E>): Boolean {
        var modified = false
        for (ele in list.toList()) {
            if (ele !in elements) {
                remove(ele)
                modified = true
            }
        }
        return modified
    }

    override fun removeAll(elements: Collection<E>): Boolean {
        var modified = false
        for (ele in list.toList()) {
            if (ele in elements) {
                remove(ele)
                modified = true
            }
        }
        return modified
    }

    override fun set(index: Int, element: E) = list.set(index, element).also {
        listChange.value = Triple(index, 1, REMOVE)
        listChange.value = Triple(index, 1, ADD)
    }
}

interface ListObserver {
    fun onAdded(start: Int, size: Int)
    fun onRemoved(start: Int, size: Int)
}

private enum class ListEvent {
    NONE, ADD, REMOVE;
}

Example usage:

val userList = MutableLiveList<User>()
.
.
.
userListAdapter = UserListAdapter(userList)
userList.observe(this, object : ListObserver {
    override fun onAdded(start: Int, size: Int) {
        userListAdapter.notifyItemRangeInserted(start, size)
    }

    override fun onRemoved(start: Int, size: Int) {
        userListAdapter.notifyItemRangeRemoved(start, size)
    }
})

This is working fine for now in main thread. I suspect would this work on background thread or not! Is this good enough? Are there any lurking bugs?

EDIT: Added two lines in the code. I don't know if these two lines are necessarily required (I tried with and without, both worked well). Without this line, whenever a new observer is added, it will get a redundant onAdded/onRemoved event depending on the recent change made to the list, before attaching the observer.

\$\endgroup\$
2
  • \$\begingroup\$ @Tenfour04 huh.. kindof. I don't know how to write unit tests. Actually I doubt something in this code, which I have asked here. If possible answer that please. \$\endgroup\$ – Sourav Kannantha B Apr 25 at 16:13
  • 1
    \$\begingroup\$ Oops, sorry, I missed which Exchange site we’re on because I’m using the app. Nothing wrong with what you’re asking here. \$\endgroup\$ – Tenfour04 Apr 25 at 17:15
0
\$\begingroup\$

You could define the class constructor this way to avoid manually delegating those basic functions:

class MutableLiveList<E> private constructor(private val list: MutableList<E>) : MutableList<E> by list {
    constructor(): this(mutableListOf())

    //...
}

If you want to be able to modify the list from a background thread, you should use postValue instead of setValue on the MutableLiveData.

You might consider designing this to be observed using a LifecycleCoroutineScope instead of a LifecycleOwner. Then you can back it with a MutableFlow and eliminate the NONE case. Your observe function could use launchWhenStarted and collect the flow to the passed listener.

One possible gotcha I see is if retainAll or removeAll is used on this List, it will fire the observer for each individual item removed. Not just with the class functions, but also the inline extension function overloads that take a lambda. But maybe that's the design intent.

Since your removeAll and retainAll rely on remove, they run in O(m*n^2) time. You might want to remove directly using the iterator so you can avoid copying the list and searching for each removed element.

\$\endgroup\$
6
  • \$\begingroup\$ Thank you for that interface delegation suggestion. But I didn't quite get that paragraphs about removeAll and retainAll. Mainly that thing about avoiding copying the list. Btw, I know implementation of those two function will have worst performance. But since those removed elements are not continuous, I have to fire observer for individual elements. Otherwise I have to throw UnsupportedOperationException. \$\endgroup\$ – Sourav Kannantha B May 13 at 17:22
  • \$\begingroup\$ toList() copies the list into another list. Then each in call searches the list. Then each remove call indirectly uses indexOf, so you have O(m*n^2) complexity. If you do something like override fun removeAll(elements: Collection<E>) = list.removeAll { it in elements }, then it will only be O(m*n) complexity and no list copy is required. \$\endgroup\$ – Tenfour04 May 13 at 17:43
  • \$\begingroup\$ I don't know how removeAll(predicate:..) is implemented. But here, for every being removed, observer should be fired. Does that happen when using removeAll? \$\endgroup\$ – Sourav Kannantha B May 13 at 17:55
  • \$\begingroup\$ Oh, good point. It won't trigger the observer for everything because it directly sets values. You would have to write your own implementation if you want to improve efficiency \$\endgroup\$ – Tenfour04 May 13 at 18:08
  • 1
    \$\begingroup\$ You can pretty much copy the code from the standard library for retainAll/removeAll which both use a private function filterInPlace. You just need to be sure that you trigger the observer correctly when removing and adding items. Also, you might want to add removeAll(predicate) and retainAll(predicate) as members of your class to prevent the standard library extension functions from accidentally being used since they will trigger your observers incorrectly. \$\endgroup\$ – Tenfour04 May 13 at 18:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.