androidmvvmandroid-pagingandroid-paging-3

Android paging 3: It is possible to get the itemcount from PagingData<T>?


how am I able to get the current amount of items, that my PagingData<Product> holds? The only solution I found was to call shopListAdapter.itemCount but this always returns 0.

What I am trying to do is: Get the current amount of items that are currently displayed by my PagingAdapter, submit this value to my shopViewModel, and then use this value inside another fragment (DialogFragment). Or: Get the amount of items from my PagingData<Product> inside my viewmodel

Here is my current approach

MainFragment

@AndroidEntryPoint
class ShopFragment : Fragment(R.layout.fragment_shop), ShopAdapter.OnItemClickListener {
    private val shopViewModel: ShopViewModel by navGraphViewModels(R.id.nav_shop) { defaultViewModelProviderFactory }
    private var _shopBinding: FragmentShopBinding? = null
    private val shopBinding: FragmentShopBinding get() = _shopBinding!!
    @Inject lateinit var shopListAdapter: ShopAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _shopBinding = FragmentShopBinding.inflate(inflater, container, false)
        return shopBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        collectShopList()
    }

    private fun collectShopList(){
        shopViewModel.shopPagingData.observe(viewLifecycleOwner)  {
            shopListAdapter.submitData(viewLifecycleOwner.lifecycle, it)
            shopViewModel.setItemAmount(shopListAdapter.itemCount)
        }
    }
}

DialogFragment

@AndroidEntryPoint
class ShopFilterFragment : DialogFragment() {
    private val shopViewModel: ShopViewModel by navGraphViewModels(R.id.nav_shop) { defaultViewModelProviderFactory }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_shop_filter, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        shopViewModel.itemAmount.observe(viewLifecycleOwner) {
            toast(it.toString()) <-- Always displays 0
        }
    }

}

ViewModel

class ShopViewModel @ViewModelInject constructor(
    private val shopRepository: ShopRepository
) : ViewModel() {
    private val _itemAmount = MutableLiveData<Int>()
    val itemAmount: LiveData<Int> get() = _itemAmount

    private val query = MutableLiveData(QueryHolder(null, null, null))

    val shopPagingData = query.switchMap { query -> shopRepository.search(query).cachedIn(viewModelScope) }

    fun search(newQuery: QueryHolder) {
        query.value = newQuery
    }

    fun setItemAmount(amount: Int) {
        _itemAmount.value = amount
    }
}

Solution

  • adapter.itemCount is the correct way, but note that there are possibilities for it to race with what the adapter has received (page will load and then notify adapter - if you call .itemCount between those steps it will return the incorrect number).

    To wait for adapter to present the page, the easiest way is to use loadStateListener / loadStateFlow, as LoadState updates are guaranteed to be synchronous with adapter events.

    shopListAdapter.addLoadStateListener { combinedLoadStates ->
      // If you don't want to call all the time, you
      // can filter on changes in combinedLoadStates
      shopViewModel.setItemAmount(shopListAdapter.itemCount)
    }
    

    Don't forget to unregister in case of leaks.

    Alternatively, you can use flow based approach:

    .. onViewCreated(..) {
      viewLifecycleOwner.lifecycleScope.launch {
        shopListAdapter.loadStateFlow.collect { combinedLoadStates ->
          // If you don't want to call all the time, you
          // can filter on changes in combinedLoadStates
          shopViewModel.setItemAmount(shopListAdapter.itemCount)
        }
      }
    }