Paging library 3.0 : How to pass total count of items to the list header?
Asked Answered
A

7

10

Help me please.
The app is just for receiving list of plants from https://trefle.io and showing it in RecyclerView.
I am using Paging library 3.0 here.
Task: I want to add a header where total amount of plants will be displayed.
The problem: I just cannot find a way to pass the value of total items to header.

    Data model:  
    data class PlantsResponseObject(
    @SerializedName("data")
    val data: List<PlantModel>?,
    @SerializedName("meta")
    val meta: Meta?
) {
    data class Meta(
        @SerializedName("total")
        val total: Int? // 415648
    )
}
   data class PlantModel(
    @SerializedName("author")
    val author: String?,
    @SerializedName("genus_id")
    val genusId: Int?, 
    @SerializedName("id")
    val id: Int?)

DataSource class:

class PlantsDataSource(
    private val plantsApi: PlantsAPI,
    private var filters: String? = null,
    private var isVegetable: Boolean? = false

) : RxPagingSource<Int, PlantView>() {

    override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, PlantView>> {
        val nextPageNumber = params.key ?: 1
           return plantsApi.getPlants(  //API call for plants
               nextPageNumber, //different filters, does not matter
               filters,
               isVegetable)
               .subscribeOn(Schedulers.io())
               .map<LoadResult<Int, PlantView>> {
                   val total = it.meta?.total ?: 0 // Here I have an access to the total count
 //of items, but where to pass it?
                    LoadResult.Page(
                       data = it.data!! //Here I can pass only plant items data
                           .map { PlantView.PlantItemView(it) },
                       prevKey = null,
                       nextKey = nextPageNumber.plus(1)
                   )
               }
               .onErrorReturn{
                   LoadResult.Error(it)
               }
    }

    override fun invalidate() {
        super.invalidate()
    }
}

LoadResult.Page accepts nothing but list of plant themselves. And all classes above DataSource(Repo, ViewModel, Activity) has no access to response object.
Question: How to pass total count of items to the list header?
I will appreciate any help.

Agrigento answered 20/9, 2020 at 10:48 Comment(1)
Did you find a solution for this??Oestriol
R
2

You can change the PagingData type to Pair<PlantView,Int> (or any other structure) to add whatever information you need. Then you will be able to send total with pages doing something similar to:

LoadResult.Page(
   data = it.data.map { Pair(PlantView.PlantItemView(it), total) },
   prevKey = null,
   nextKey = nextPageNumber.plus(1)
)

And in your ModelView do whatever, for example map it again to PlantItemView, but using the second field to update your header.

It's true that it's not very elegant because you are sending it in all items, but it's better than other suggested solutions.

Restrictive answered 4/3, 2021 at 16:50 Comment(0)
S
1

One way is to use MutableLiveData and then observe it. For example

val countPlants = MutableLiveData<Int>(0)

override fun loadSingle(..... {

    countPlants.postValue(it.meta?.total ?: 0)

} 

Then somewhere where your recyclerview is.

pagingDataSource.countPlants.observe(viewLifecycleOwner) { count ->
    //update your view with the count value
}
Senhor answered 20/9, 2020 at 11:3 Comment(0)
O
1

Faced the same dilemma when trying to use Paging for the first time and it does not provide a way to obtain count despite it doing a count for the purpose of the paging ( i.e. the Paging library first checks with a COUNT(*) to see if there are more or less items than the stipulated PagingConfig value(s) before conducting the rest of the query, it could perfectly return the total number of results it found ).

The only way at the moment to achieve this is to run two queries in parallel: one for your items ( as you already have ) and another just to count how many results it finds using the same query params as the previous one, but for COUNT(*) only.

There is no need to return the later as a PagingDataSource<LivedData<Integer>> since it would add a lot of boilerplate unnecessarily. Simply return it as a normal LivedData<Integer> so that it will always be updating itself whenever the list results change, otherwise it can run into the issue of the list size changing and that value not updating after the first time it loads if you return a plain Integer.

After you have both of them set then add them to your RecyclerView adapter using a ConcatAdapter with the order of the previously mentioned adapters in the same order you'd want them to be displayed in the list.

ex: If you want the count to show at the beginning/top of the list then set up the ConcatAdapter with the count adapter first and the list items adapter after.

Oder answered 22/10, 2020 at 14:35 Comment(0)
R
0

The withHeader functions in Paging just return a ConcatAdapter given a LoadStateHeader, which has some code to listen and update based on adapter's LoadState.

You should be able to do something very similar by implementing your own ItemCountAdapter, except instead of listening to LoadState changes, it listens to adapter.itemCount. You'll need to build a flow / listener to decide when to send updates, but you can simply map loadState changes to itemCount.

See here for LoadStateAdapter code, which you can basically copy, and change loadState to itemCount: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:paging/runtime/src/main/java/androidx/paging/LoadStateAdapter.kt?q=loadstateadapter

e.g.,

abstract class ItemCountAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() { 

var itemCount: Int = 0

set(itemCount { ... }

open fun displayItemCountAsItem(itemCount: Int): Boolean { 
    return true 
}

...

Then to actually create the ConcatAdapter, you want something similar to: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:paging/runtime/src/main/java/androidx/paging/PagingDataAdapter.kt;l=236?q=withLoadStateHeader&sq=

fun PagingDataAdapter.withItemCountHeader(itemCountAdapter): ConcatAdapter {
    addLoadStateListener {
        itemCountAdapter.itemCount = itemCount
    }
    return ConcatAdapter(itemCountAdapter, this)
}
Reider answered 20/9, 2020 at 17:50 Comment(2)
adapter.itemCount has nothing to do with my question. Total amount of plants is about 300,000 and I want to show this value in adapter right after first portion of 30 plants is loaded. I extract total amount of plants from Meta object in server response.Agrigento
I see, sorry if I misread your question. Regardless of how you populate / query the plant total the general idea should still be similar. i.e., ConcatAdapter produced with PagingDataAdapter + something very similar to LoadStateAdapter, but customized to show when initial load completes and is not empty instead of simply listening to Prepend / append load state updates. Let me know if there's anything I can clarify.Reider
R
0

Another solution, although also not very elegant, would be to add the total amount to your data model PlantView.

PlantView(…val totalAmount: Int…)

Then in your viewmodel you could add a header with the information of one item. Here is a little modified code taken from the official paging documenation

pager.flow.map { pagingData: PagingData<PlantView> ->
  // Map outer stream, so you can perform transformations on
  // each paging generation.
  pagingData
  .map { plantView ->
    // Convert items in stream to UiModel.PlantView.
    UiModel.PlantView(plantView)
  }
  .insertSeparators<UiModel.PlantView, UiModel> { before, after ->
    when {
      //total amount is used from the next PlantView
      before == null -> UiModel.SeparatorModel("HEADER", after?.totalAmount)
      // Return null to avoid adding a separator between two items.
      else -> null
    }
  }
}

A drawback is the fact that the total amount is in every PlantView and it's always the same and therefore redundant.

Reardon answered 6/10, 2021 at 8:57 Comment(0)
O
0

For now, I found this comment usefull: https://issuetracker.google.com/issues/175338415#comment5

There people discuss the ways to provide metadata state to Pager

Osborn answered 22/10, 2021 at 16:57 Comment(0)
W
0

A simple way I found to fix it is by using a lambda in the PagingSource constructor. Try the following:

class PlantsDataSource(
    // ...
    private val getTotalItems: (Int) -> Unit
) : RxPagingSource<Int, PlantView>() {

    override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, PlantView>> {
           ...
           .map<LoadResult<Int, PlantView>> {
               val total = it.meta?.total ?: 0
               getTotalItems(total)

               ...
           }
           ...
    }

}
Walli answered 25/11, 2022 at 17:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.