Paging 3 initial loading not shown
Asked Answered
R

5

12

I am working with paging 3, everything work fine except initial loading state. I am adding withLoadStateFooter but it never show loading state at first call

Here is my implementation

Load State Adapter

class LoadStateAdapter (
    private val retry: () -> Unit
): LoadStateAdapter<LoadStateViewHolder>() {

    override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {
        holder.bindTo(loadState)
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        loadState: LoadState
    ): LoadStateViewHolder {
        return LoadStateViewHolder.create(parent, retry)
    }
}

Load State View Holder

class LoadStateViewHolder(
    view : View,
    private val retryCallback: () -> Unit
) : RecyclerView.ViewHolder(view) {

    private val progressBar = view.findViewById<ProgressBar>(R.id.progress_bar)
    private val errorMsg = view.findViewById<TextView>(R.id.error_msg)
    private val btnRetry = view.findViewById<Button>(R.id.retry_button)
        .also {
            it.setOnClickListener { retryCallback() }
        }
    private var loadState : LoadState? = null

    companion object {
        fun create(parent: ViewGroup, retryCallback: () -> Unit): LoadStateViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.network_state_item, parent, false)
            return LoadStateViewHolder(
                view,
                retryCallback
            )
        }
    }


    fun bindTo(loadState: LoadState) {
        this.loadState = loadState

        btnRetry.isVisible = loadState !is LoadState.Loading
        errorMsg.isVisible = loadState !is LoadState.Loading
        progressBar.isVisible = loadState is LoadState.Loading

        if (loadState is LoadState.Error){
            errorMsg.text = loadState.error.localizedMessage
        }
    }
}

Paging Source

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Model> {

    try {
        // Load page 1 if undefined.
        val currentPage = params.key ?: 0
        val offset = currentPage * 50

        val requestParams = hashMapOf<String, Any>()
        requestParams.put("limit", 50)
        requestParams.put("offset", offset)

        val response =  repository.getList(requestParams)
        val isFinish = response.paging != null && response.paging!!.next == null

        return LoadResult.Page(
            data = response.data ?: mutableListOf(),
            prevKey = null, // Only paging forward.
            nextKey = if (isFinish) null else currentPage + 1
        )
    } catch (e: Exception) {
        // Handle errors in this block
        return LoadResult.Error(e)
    }
}

View Model

val listPagingFlow = Pager(PagingConfig(pageSize = 50)) {
    MyPagingSource(repository)
}.flow.cachedIn(viewModelScope)

Activity

    val pagingAdapter = MyPagingAdapter()
    list.apply {
        setHasFixedSize(true)
        adapter = pagingAdapter.withLoadStateFooter(
            footer = LoadStateAdapter { pagingAdapter.retry() }
        )
    }

    lifecycleScope.launch(Dispatchers.IO) {
        viewModel.listPagingFlow.collectLatest { pagingData ->
            pagingAdapter.submitData(pagingData)
        }
    }

MyPagingAdapter is simple PagingDataAdapter

In short; loading state works fine but it did not showing at first request. Can any one help?

Current version 3.0.0-alpha04

Raeraeann answered 14/8, 2020 at 19:7 Comment(0)
A
13

withLoadStateFooter returns a ConcatAdapter which concatenates results from original PagingDataAdapter with a LoadStateAdapter that listens to CombinedLoadState.append events. So it's not expected for it to return an item during initial load (loadType == REFRESH), and it was designed this way because it doesn't really make sense to show a "footer" before any items has loaded.

However, to achieve what you want you can simply create your own ConcatAdapter which mirrors the implementation of .withLoadStateFooter very closely:

val originalAdapter = MyPagingDataAdapter(...)
val footerLoadStateAdapter = MyFooterLoadStateAdapter(...)

addLoadStateListener { loadStates ->
    // You need to implement some logic here to update LoadStateAdapter.loadState
    // based on however you want to prioritize between REFRESH / APPEND load states.
    // Something really basic might be:
    // footerLoadStateAdapter.loadState = when {
    //     loadStates.refresh is NotLoading -> loadStates.append
    //     else -> loadStates.refresh
    // }
    footerLoadStateAdapter.loadState = ...
}
return ConcatAdapter(originalAdapter, footerLoadStateAdapter)
Adsorbate answered 17/8, 2020 at 21:52 Comment(4)
I didn't understand the answer. Can you give me some references or links?Waw
Neither did I, I'm having a similar problem but cant understand a propper solution, any help?Garlic
How can I clarify the sample code above? A ConcatAdapter combines the data from multiple adapters into one, which let's you display data from multiple adapters in a RecyclerView. LoadStateAdapter is a built-in from the Paging library which displays 0 or 1 items depending on what displayLoadStateAsItem returns, and you can set the state via the public mutable property. See developer.android.com/reference/kotlin/androidx/paging/…Adsorbate
This sample is showing an example of how to customize the default behavior of LoadStateAdapter, so you can choose when it switches to another LoadState. Some people may be interested in a combination of LoadStates from different LoadTypes depending on their use-case.Adsorbate
F
8

Using the footer LoadStateAdapter for showing initial loading will cause issues. The list will automatically be scrolled to the bottom as mentioned in the comments.

The way to go about this is to use two LoadStateAdapters, one to show the initial loading and the second to show loading when more items are being loaded.

fun <T : Any, V : RecyclerView.ViewHolder> PagingDataAdapter<T, V>.withLoadStateAdapters(
    header: LoadStateAdapter<*>,
    footer: LoadStateAdapter<*>
): ConcatAdapter {
    addLoadStateListener { loadStates ->
        header.loadState = loadStates.refresh
        footer.loadState = loadStates.append
    }

    return ConcatAdapter(header, this, footer)
}
Fluted answered 23/3, 2021 at 12:49 Comment(0)
K
5

I would suggest lifting the initial loading and empty state out of the paging adapter and do it like this:

adapter.loadStateFlow.collect {
    // to determine if we are done with the loading state, 
    // you should have already  shown your loading view elsewhere when the entering your fragment
    if (it.prepend is LoadState.NotLoading && it.prepend.endOfPaginationReached) {
        loading.visibility = View.GONE
    }
    if (it.append is LoadState.NotLoading && it.append.endOfPaginationReached) {
        emptyState.isVisible = adapter.itemCount < 1
    }
}

The logic is

  • If the append has finished (it.append is LoadState.NotLoading && it.append.endOfPaginationReached == true), and our adapter items count is zero (adapter.itemCount < 1), means there is nothing to show, so we show the empty state
  • If the prepend has finished (it.prepend is LoadState.NotLoading && it.prepend .endOfPaginationReached == true), then we are already done with the initial loading view and we should hide it
Kerb answered 23/5, 2021 at 15:11 Comment(0)
M
1

As @dlam answer you can use custom ConcatAdapter in your adapter instead of withLoadStateFooter

fun withMySpecificFooter(
    footer: LoadStateAdapter<*>
): ConcatAdapter {
    addLoadStateListener { loadStates ->
        footer.loadState = when (loadStates.refresh) {
            is LoadState.NotLoading -> loadStates.append
            else -> loadStates.refresh
        }
    }
    return ConcatAdapter(this, footer)
}
Mccullers answered 24/8, 2020 at 12:42 Comment(3)
it's working but why list items showing from middle? i.e if i have 10 item, recyclerview showing 5th item in initial and must scroll to top for seeing first item.Sideburns
@HadiAhmadi same problemQuadrangle
I encountered the same problem with RecyclerView starting at the middle whenever withLoadStateFooter() is used. Does anyone know how to fix this issue? Thanks in advance.Gabrielgabriela
A
0

I added CircularProgressIndicator to my layout and show it by calling its show() method when I request for data, then hide it with:

lifecycleScope.launch {
    listAdapter.loadStateFlow.collect {
       if(it.refresh is LoadState.NotLoading) loadingIndicator.hide()
    }
 }

The CircularProgressIndicator in xml has property: visibility="invisible"

Amaro answered 29/6 at 6:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.