18

I'm currently looking into incorporating the Paging Architecture library (version 2.1.0-beta01 at the time of writing) into my app. One components is a list which allows the user to delete individual items from it. This list is network-only and caching localy with Room does not make sense.

PagedList is immutable and does not support modification. I have read that having a copy of the list which is than modified and returned as the new one is the way to go. The documentation states the same:

If you have more granular update signals, such as a network API signaling an update to a single item in the list, it's recommended to load data from the network into memory. Then present that data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the snapshot can be created.

I currently have the basic recommended implementation to show a simple list. My DataSource looks like this:

class MyDataSource<SomeItem> : PageKeyedDataSource<Int, SomeItem>() {

    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, SomeItem>) {
        // Simple load from API and notification of `callback`.
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, SomeItem>) {
        // Simple load from API and notification of `callback`.
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, SomeItem>) {
        // Simple load from API and notification of `callback`.
    }
}

How would a concrete implementation of an in-memory cache (without Room and without invalidating the entire dataset) as referenced in the documentation look like?

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
rubengees
  • 1,841
  • 2
  • 16
  • 31

2 Answers2

7

If you want to modify your list without going all the way down to the data layer, you will need to override submitList in your adapter, and then set a callback on your PagedList object. Whenever the PagedList changes, you can then copy those changes to your local dataset. This is not recommended but it's a pretty minimal hack to get working.

Here's an example:

class MyListAdapter : PagedListAdapter<MyDataItem, MyViewHolder>(MyDiffCallback()) {

    /**
     * This data set is a bit of a hack -- we are copying everything the PagedList loads into our
     * own list.  That way we can modify it.  The docs say you should go all the way down to the
     * data source, modify it there, and then bubble back up, but I don't think that will actually
     * work for us when the changes are coming from the UI itself.
     */
    private val dataSet = arrayListOf<MyDataItem>()

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        //Forces the next page to load when we reach the bottom of the list
        getItem(position)

        dataSet.getOrNull(position)?.let {
            holder.populateFrom(it)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val view = parent.inflate(R.layout.my_view_holder)
        return MyViewHolder(view)
    }

    class MyDiffCallback : DiffUtil.ItemCallback<MyDataItem>() {

        override fun areItemsTheSame(oldItem: MyDataItem, newItem: MyDataItem) =
                oldItem.id == newItem.id

        override fun areContentsTheSame(oldItem: MyDataItem, newItem: MyDataItem) =
                oldItem == newItem
    }

    override fun submitList(pagedList: PagedList<MyDataItem>?) {
        pagedList?.addWeakCallback(listOf(), object : PagedList.Callback() {
            override fun onChanged(position: Int, count: Int) {
                dataSet.clear()
                dataSet.addAll(pagedList)
            }

            override fun onInserted(position: Int, count: Int) {
                dataSet.clear()
                dataSet.addAll(pagedList)
            }

            override fun onRemoved(position: Int, count: Int) {
                dataSet.clear()
                dataSet.addAll(pagedList)
            }
        })
        super.submitList(pagedList)
    }
}
PhillyTheThrilly
  • 1,562
  • 2
  • 16
  • 21
  • 1
    Why are you cleaning the data set each time? – masterwok Oct 28 '19 at 22:04
  • 3
    @masterwok Google built the paging library in a very strange way. `submitList` only gets called one time, and all of the page loading is done inside the `PagedList` class. So the `pagedList` instance here always holds the latest updates to the list, and we are using the `PagedList.Callback` to know when to update `dataSet`. We want `dataSet` to mirror `pagedList`, so we clear it and add everything from `pagedList`. – PhillyTheThrilly Oct 29 '19 at 16:18
  • how you modified values in the PagedList< MyDataItem> which is passed to submitList() in the adapter class? I am loading data from the network with no database. I have to modify the property of the POJO class held by the PagedList based on user action and update the recyclerview. So I would like to know how to modify the pagedList and update UI – android.fryo Feb 25 '20 at 20:39
  • @android.fryo you will want to leave `submitList` alone and modify `dataSet` instead. `submitList` should only get called one time. – PhillyTheThrilly Feb 26 '20 at 21:05
  • how we can modify data set? PagedList is immutable. – android.fryo Feb 27 '20 at 00:18
  • Modify the `dataSet` property as in the example above – PhillyTheThrilly Mar 02 '20 at 19:36
  • 1
    Thanks man! save my day. also we need to return the dataSet size as the list count: ```override fun getItemCount(): Int { return dataSet.size } ``` – boni octavianus May 26 '20 at 09:27
  • @bonioctavianus can you please share your code... its pretty urgent – Samin May 29 '20 at 12:44
  • any example in java ? – Zhar Aug 15 '20 at 19:14
  • where is holder. populateFrom declared ? – Gastón Saillén Apr 16 '21 at 15:23
  • @GastónSaillén you will have to declare that yourself in your ViewHolder – PhillyTheThrilly May 23 '22 at 22:44
4

You are correct in that a DataSource is meant to hold immutable data. I believe this is because Room and Paging Library is trying to have more opinionated design decisions and advocate for immutable data.

This is why in the official docs, they have a section for updating or mutating your dataset should invalidate the datasource when such a change occurs.

Updating Paged Data: If you have more granular update signals, such as a network API signaling an update to a single item in the list, it's recommended to load data from network into memory. Then present that data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the snapshot can be created.

Source: https://developer.android.com/reference/android/arch/paging/DataSource


With that in mind, I believe it's possible to solve the problem you described using a couple of steps.

This may not be the cleanest way, as it involves 2 steps.

You can get a reference the the snapshot that the PagedList is holding, which is a type MutableList. Then, you can just remove or update the item inside that snapshot, without invalidating the data source.

Then step two would be to calling something like notifyItemRemoved(index) or notifyItemChanged(index).

Since you can't force the DataSource to notify the observers of the change, you'll have to do that manually.

pagedList.snapshot().remove(index) // Removes item from the pagedList
adapter.notifyItemRemoved(index) // Triggers recyclerview to redraw/rebind to account for the deleted item.

There maybe a better solution found in your DataSource.Factory. According to the official docs, your DataSource.Factory should be the one to emit a new PagedList once the data is updated.

Updating Paged Data: To page in data from a source that does provide updates, you can create a DataSource.Factory, where each DataSource created is invalidated when an update to the data set occurs that makes the current snapshot invalid. For example, when paging a query from the Database, and the table being queried inserts or removes items. You can also use a DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data, you can connect an explicit refresh signal to call invalidate() on the current DataSource.

Source: https://developer.android.com/reference/android/arch/paging/DataSource

I haven't found a good solution for this second approach however.

Felipe Roriz
  • 402
  • 3
  • 5
  • 10
    Thank you for this detailed answer, but this approach does not work. The snapshot returned may be of type `MutableList`, but does not actually implement the interface. Calling for example `removeAt` yields an `UnsupportedOperationException`. Also, the documentation states that snapshots are not meant for reflecting modification and are in fact also immutable. So thanks for the answer, but downvoting, because it's incorrect and may mislead other people. – rubengees Nov 23 '18 at 20:57
  • Ah, I see. Then I the only other way I can think of is the second option mentioned above, and utilizing the `DataSource.Factory`. – Felipe Roriz Nov 24 '18 at 02:49
  • Yes, that seems the way to go. I have not yet found a way of implementing that and tried to get help on that with this question. – rubengees Nov 25 '18 at 00:21
  • @rubengees Have you found a solution? – Leo DroidCoder May 31 '19 at 14:52
  • @LeoDroidcoder No, I am not using the paging library in my app yet (until something like this is possible). – rubengees Jun 01 '19 at 11:21
  • You can actually modify items inside the list – MobDev Sep 21 '19 at 01:06