Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PagingWithNetworkSample - PagedList RecyclerView scroll bug #548

Closed
lucandr opened this issue Feb 5, 2019 · 16 comments
Closed

PagingWithNetworkSample - PagedList RecyclerView scroll bug #548

lucandr opened this issue Feb 5, 2019 · 16 comments
Labels

Comments

@lucandr
Copy link

lucandr commented Feb 5, 2019

Hi guys, I found a problem on the scroll when I don't let the data load and go to another activity and then go back to the previous one.

You can see this on this video.
Video showing the problem

Can u help me to solve this?

Cheers!

@onlymash
Copy link

onlymash commented Feb 6, 2019

Same issue is here
PostAdapter

class PostAdapter(private val glide: GlideRequests,
                  private val placeholder: Placeholder,
                  private val listener: PostViewHolder.ItemListener,
                  private val retryCallback: () -> Unit) : PagedListAdapter<Any, RecyclerView.ViewHolder>(POST_COMPARATOR) {

    companion object {
        val POST_COMPARATOR = object : DiffUtil.ItemCallback<Any>() {
            override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean = oldItem == newItem
            override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
                return when {
                    oldItem is PostDan && newItem is PostDan -> oldItem.id == newItem.id
                    oldItem is PostMoe && newItem is PostMoe -> oldItem.id == newItem.id
                    else -> false
                }
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            R.layout.item_header -> HeaderViewHolder.create(parent)
            R.layout.item_post -> PostViewHolder.create(parent, glide, placeholder)
            R.layout.item_network_state -> NetworkStateViewHolder.create(parent, retryCallback)
            else -> throw IllegalArgumentException("unknown view type $viewType")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (getItemViewType(position)) {
            R.layout.item_header -> {
                if (holder.itemView.layoutParams is StaggeredGridLayoutManager.LayoutParams) {
                    (holder.itemView.layoutParams as StaggeredGridLayoutManager.LayoutParams).isFullSpan = true
                }
            }
            R.layout.item_post -> {
                (holder as PostViewHolder).bind(getItem(position - 1))
                holder.setItemListener(listener)
            }
            R.layout.item_network_state -> {
                if (holder.itemView.layoutParams is StaggeredGridLayoutManager.LayoutParams) {
                    (holder.itemView.layoutParams as StaggeredGridLayoutManager.LayoutParams).isFullSpan = true
                }
                (holder as NetworkStateViewHolder).bindTo(networkState)
            }
        }
    }

    private var networkState: NetworkState? = null

    private fun hasExtraRow() = networkState != null && networkState != NetworkState.LOADED

    override fun getItemViewType(position: Int): Int {
        return if (position == 0) {
            R.layout.item_header
        } else if (hasExtraRow() && position == itemCount - 1) {
            R.layout.item_network_state
        } else {
            R.layout.item_post
        }
    }

    override fun getItemCount(): Int {
        return super.getItemCount() + if (hasExtraRow()) 2 else 1
    }

    fun setNetworkState(newNetworkState: NetworkState?) {
        val previousState = this.networkState
        val hadExtraRow = hasExtraRow()
        this.networkState = newNetworkState
        val hasExtraRow = hasExtraRow()
        if (hadExtraRow != hasExtraRow) {
            if (hadExtraRow) {
                notifyItemRemoved(super.getItemCount() + 1)
            } else {
                notifyItemInserted(super.getItemCount() + 1)
            }
        } else if (hasExtraRow && previousState != newNetworkState) {
            notifyItemChanged(itemCount - 1)
        }
    }
}

@onlymash
Copy link

onlymash commented Feb 7, 2019

I have solved my issue like this

class PostAdapter(private val glide: GlideRequests,
                  private val placeholder: Placeholder,
                  private val listener: PostViewHolder.ItemListener,
                  private val retryCallback: () -> Unit) : PagedListAdapter<Any, RecyclerView.ViewHolder>(POST_COMPARATOR) {

    companion object {
        val POST_COMPARATOR = object : DiffUtil.ItemCallback<Any>() {
            override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean = oldItem == newItem
            override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
                return when {
                    oldItem is PostDan && newItem is PostDan -> oldItem.id == newItem.id
                    oldItem is PostMoe && newItem is PostMoe -> oldItem.id == newItem.id
                    else -> false
                }
            }
        }
    }

    private val adapterCallback = AdapterListUpdateCallback(this)

    private val listUpdateCallback = object : ListUpdateCallback {
        override fun onChanged(position: Int, count: Int, payload: Any?) {
            adapterCallback.onChanged(position + 1, count, payload)
        }

        override fun onMoved(fromPosition: Int, toPosition: Int) {
            adapterCallback.onMoved(fromPosition + 1, toPosition + 1)
        }

        override fun onInserted(position: Int, count: Int) {
            adapterCallback.onInserted(position + 1, count)
        }

        override fun onRemoved(position: Int, count: Int) {
            adapterCallback.onRemoved(position + 1, count)
        }
    }

    private val differ = AsyncPagedListDiffer<Any>(listUpdateCallback,
        AsyncDifferConfig.Builder<Any>(POST_COMPARATOR).build())

    override fun getItem(position: Int): Any? {
        return differ.getItem(position - 1)
    }

    override fun submitList(pagedList: PagedList<Any>?) {
        differ.submitList(pagedList)
    }

    override fun getCurrentList(): PagedList<Any>? {
        return differ.currentList
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            R.layout.item_header -> HeaderViewHolder.create(parent)
            R.layout.item_post -> PostViewHolder.create(parent, glide, placeholder)
            R.layout.item_network_state -> NetworkStateViewHolder.create(parent, retryCallback)
            else -> throw IllegalArgumentException("unknown view type $viewType")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (getItemViewType(position)) {
            R.layout.item_header -> {
                if (holder.itemView.layoutParams is StaggeredGridLayoutManager.LayoutParams) {
                    (holder.itemView.layoutParams as StaggeredGridLayoutManager.LayoutParams).isFullSpan = true
                }
            }
            R.layout.item_post -> {
                (holder as PostViewHolder).bind(getItem(position))
                holder.setItemListener(listener)
            }
            R.layout.item_network_state -> {
                if (holder.itemView.layoutParams is StaggeredGridLayoutManager.LayoutParams) {
                    (holder.itemView.layoutParams as StaggeredGridLayoutManager.LayoutParams).isFullSpan = true
                }
                (holder as NetworkStateViewHolder).bindTo(networkState)
            }
        }
    }

    private var networkState: NetworkState? = null

    private fun hasExtraRow() = networkState != null && networkState != NetworkState.LOADED

    override fun getItemViewType(position: Int): Int {
        return if (position == 0) {
            R.layout.item_header
        } else if (hasExtraRow() && position == itemCount - 1) {
            R.layout.item_network_state
        } else {
            R.layout.item_post
        }
    }

    override fun getItemCount(): Int {
        return differ.itemCount + if (hasExtraRow()) 2 else 1
    }

    fun setNetworkState(newNetworkState: NetworkState?) {
        val previousState = this.networkState
        val hadExtraRow = hasExtraRow()
        this.networkState = newNetworkState
        val hasExtraRow = hasExtraRow()
        if (hadExtraRow != hasExtraRow) {
            if (hadExtraRow) {
                notifyItemRemoved(differ.itemCount + 1)
            } else {
                notifyItemInserted(differ.itemCount + 1)
            }
        } else if (hasExtraRow && previousState != newNetworkState) {
            notifyItemChanged(itemCount - 1)
        }
    }
}

@qingmei2
Copy link

qingmei2 commented Apr 2, 2019

@onlymash

Well, your solution is effective, but if I removed the Header in List(just PagedList+Footer),the list still automatically scrolled to the end of the loaded page when I refresh recyclerView successfully.

My answer is that add an empty layout header on top of RecyclerView which can't be seen by user(for example, layout_height = 0.1dp) as static placeholder. the issue was solved but it's not elegant solution in my opinion. 😢

@onlymash
Copy link

onlymash commented Apr 2, 2019

@qingmei2
So your header won't scroll, the list won't be full screen

@qingmei2
Copy link

qingmei2 commented Apr 2, 2019

@onlymash

I am sorry for I did not explain clearly.

So your header won't scroll, the list won't be full screen

exactly, no, I implement a multi type PagedList with Header and Footer.

finally I was able to build it according to this post⬇️:

Android RecyclerView + Paging Library 添加头部刷新会自动滚动的问题分析及解决

Briefly summarize,the post author create a proxy class:

class AdapterDataObserverProxy extends RecyclerView.AdapterDataObserver {
    RecyclerView.AdapterDataObserver adapterDataObserver;
    int headerCount;
    public ArticleDataObserver(RecyclerView.AdapterDataObserver adapterDataObserver, int headerCount) {
        this.adapterDataObserver = adapterDataObserver;
        this.headerCount = headerCount;
    }
    @Override
    public void onChanged() {
        adapterDataObserver.onChanged();
    }
    @Override
    public void onItemRangeChanged(int positionStart, int itemCount) {
        adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount);
    }
    @Override
    public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
        adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount, payload);
    }
    @Override
    public void onItemRangeInserted(int positionStart, int itemCount) {
        adapterDataObserver.onItemRangeInserted(positionStart + headerCount, itemCount);
    }
    @Override
    public void onItemRangeRemoved(int positionStart, int itemCount) {
        adapterDataObserver.onItemRangeRemoved(positionStart + headerCount, itemCount);
    }
    @Override
    public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
        super.onItemRangeMoved(fromPosition + headerCount, toPosition + headerCount, itemCount);
    }
}

and override the PagedListAdapter.registerAdapterDataObserver() method:

override fun registerAdapterDataObserver(observer: RecyclerView.AdapterDataObserver) {
        super.registerAdapterDataObserver(AdapterDataObserverProxy(observer, 1)) // 1 -> headerCount
}

This code part just explain how to set up Header, but same to Footer.

Emm....in principle, this approach is no different from yours. the difference is just where we deceive the RecyclerView to handle the update position range.😂

the demo code at here(Both header and footer).

@lucandr
Copy link
Author

lucandr commented Apr 3, 2019

Hey guys @qingmei2 @onlymash , I am still having this problem too and I don't know how to solve it. I fix one part but then I break another.

I have this adapter with this three ViewHolders:

class HomeAdapter @Inject
constructor(val sessionRepository: SessionRepository) :
    PagedListAdapter<HomeItem, RecyclerView.ViewHolder>(userDiffCallback) {

    private var networkState: NetworkState? = null
    private var retryCallback: RetryCallback? = null
    lateinit var recommendedAdapter: RecommendedAdapter
    var feedJobListener: JobsViewHolder.Listener? = null
    var questionListener: QuestionsViewHolder.Listener? = null

    fun setRetryCallback(retryCallback: RetryCallback) {
        this.retryCallback = retryCallback
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            R.layout.item_question -> QuestionsViewHolder.create(
                parent,
                sessionRepository,
                questionListener
            )
            R.layout.view_network_state -> NetworkStateViewHolder.create(
                parent,
                retryCallback
            )
            R.layout.item_list_recommended -> RecommendedViewHolder.create(parent)
            R.layout.item_feed_job -> JobFeedViewHolder.create(
                parent,
                feedJobListener,
                sessionRepository.isLoggedUserJobSeeker()
            )
            else -> throw IllegalArgumentException("unknown view type")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (getItemViewType(position)) {
            R.layout.item_question -> (holder as QuestionsViewHolder)
                .bindTo(getItem(position) as QuestionResponse)
            R.layout.view_network_state -> (holder as NetworkStateViewHolder)
                .bindTo(networkState)
            R.layout.item_list_recommended -> (holder as RecommendedViewHolder)
                .bindTo(getItem(position) as Recommended?, recommendedAdapter)
            R.layout.item_feed_job -> (holder as JobFeedViewHolder)
                .bindTo(position, getItem(position) as JobResponse)
        }
    }

    private fun hasExtraRow(): Boolean {
        return networkState != null && networkState != NetworkState.LOADED
    }

    override fun getItemViewType(position: Int): Int {
        return if (hasExtraRow() && position == itemCount - 1) {
            R.layout.view_network_state
        } else if (position == 0) {
            R.layout.item_list_recommended
        } else if (getItem(position) is QuestionResponse) {
            R.layout.item_question
        } else
            R.layout.item_feed_job
    }

    override fun getItemCount(): Int {
        return super.getItemCount() + if (hasExtraRow()) 1 else 0
    }

    fun setNetworkState(newNetworkState: NetworkState?) {
        val previousState = this.networkState
        val hadExtraRow = hasExtraRow()
        this.networkState = newNetworkState
        val hasExtraRow = hasExtraRow()
        if (hadExtraRow != hasExtraRow) {
            if (hadExtraRow) {
                notifyItemRemoved(super.getItemCount())
            } else {
                notifyItemInserted(super.getItemCount())
            }
        } else if (hasExtraRow && previousState !== newNetworkState) {
            notifyItemChanged(itemCount - 1)
        }
    }

    companion object {

        private val userDiffCallback = object : DiffUtil.ItemCallback<HomeItem>() {

            override fun areItemsTheSame(oldItem: HomeItem, newItem: HomeItem): Boolean {
                return oldItem == newItem
            }

            override fun areContentsTheSame(oldItem: HomeItem, newItem: HomeItem): Boolean {
                return oldItem == newItem
            }
        }
    }
}

If I add this lines to the adapter (the fix from qingmei2), the problem is fixed but I have a problem with the swipe refresh:

    override fun registerAdapterDataObserver(observer: RecyclerView.AdapterDataObserver) {
        super.registerAdapterDataObserver(AdapterDataObserverProxy(observer, 1))
    }

When I swipe to refresh I get this:

2019-04-03 09:17:41.114 501-501/com.myapp.android E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.myapp.android, PID: 501
    java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{66c78b1 position=0 id=-1, oldPos=0, pLpos:-1 scrap [attachedScrap] tmpDetached no parent} androidx.recyclerview.widget.RecyclerView{f2f2c58 VFED.V... ......I. 0,0-1080,1665 #7f0a0198 app:id/list_main}, adapter:com.myapp.android.features.home.home.adapter.HomeAdapter@8e1e0b6, layout:androidx.recyclerview.widget.LinearLayoutManager@d6851b7, context:com.myapp.android.features.home.HomeActivity@b27dc41
        at androidx.recyclerview.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:5715)
        at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5898)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5858)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5854)
        at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2230)
        at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1557)
        at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1517)
        at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:612)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep1(RecyclerView.java:3875)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3639)
        at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4194)
        at android.view.View.layout(View.java:20836)
        at android.view.ViewGroup.layout(ViewGroup.java:6401)
        at androidx.swiperefreshlayout.widget.SwipeRefreshLayout.onLayout(SwipeRefreshLayout.java:625)
        at android.view.View.layout(View.java:20836)
        at android.view.ViewGroup.layout(ViewGroup.java:6401)
        at android.widget.RelativeLayout.onLayout(RelativeLayout.java:1080)
        at android.view.View.layout(View.java:20836)
        at android.view.ViewGroup.layout(ViewGroup.java:6401)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
        at android.view.View.layout(View.java:20836)
        at android.view.ViewGroup.layout(ViewGroup.java:6401)
        at androidx.constraintlayout.widget.ConstraintLayout.onLayout(ConstraintLayout.java:1915)
        at android.view.View.layout(View.java:20836)
        at android.view.ViewGroup.layout(ViewGroup.java:6401)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
        at android.view.View.layout(View.java:20836)
        at android.view.ViewGroup.layout(ViewGroup.java:6401)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1544)
        at android.view.View.layout(View.java:20836)
        at android.view.ViewGroup.layout(ViewGroup.java:6401)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
        at android.view.View.layout(View.java:20836)
        at android.view.ViewGroup.layout(ViewGroup.java:6401)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1544)
        at android.view.View.layout(View.java:20836)
        at android.view.ViewGroup.layout(ViewGroup.java:6401)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
        at com.android.internal.policy.DecorView.onLayout(DecorView.java:944)
        at android.view.View.layout(View.java:20836)
        at android.view.ViewGroup.layout(ViewGroup.java:6401)
        at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2948)
2019-04-03 09:17:41.115 501-501/com.myapp.android E/AndroidRuntime:     at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2635)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1779)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7810)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:911)
        at android.view.Choreographer.doCallbacks(Choreographer.java:723)
        at android.view.Choreographer.doFrame(Choreographer.java:658)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:897)
        at android.os.Handler.handleCallback(Handler.java:789)
        at android.os.Handler.dispatchMessage(Handler.java:98)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6938)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)

How can I solve this?

@qingmei2
Copy link

qingmei2 commented Apr 4, 2019

hi, @lucandr :)

I have read your code but not know clearly where was error...

I found the codes' logic is managing a dynamic row as Footer in the list, have you try make this row as static instead of dynamic managing this row added or removed( just control it Visibility or Gone)?

Maybe it is not best solution but I think it simplify the logic at least, and I always know the number of item's count:

   override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is HeaderViewHolder -> holder.bindsHeader()   // header
            is FooterViewHolder -> holder.bindsFooter()      // footer 
            is StudentViewHolder -> holder.bindTo(getStudentItem(position))  // main list
        }
    }

    private fun getStudentItem(position: Int): Student? {
        return getItem(position - 1)       //  position = 1 means fetch first(position 0) student in pagedList.
    }

    override fun getItemCount(): Int {
        return super.getItemCount() + 2    // +2 means static header and static footer
    }

see detail here :)

"just control it Visibility or Gone" maybe like this:

class Footer: RecyclerView.ViewHolder {
      // .....
      fun binds(position:Int){
            rootView.visibility = if(position == 0) View.VISIBLE else View.GONE
      }
}

Can this solve your issue? or you can try to describe your problem in detail. :)

@lucandr
Copy link
Author

lucandr commented Apr 15, 2019

@qingmei2 can u watch the video that I posted on the first post and try to run the code of this repo?
Repo
Video

@parcool
Copy link

parcool commented Aug 12, 2019

I got this issue too and is there any perfect way to fix it? Is it a big bug to so hard to do?

@parcool
Copy link

parcool commented Aug 13, 2019

Is there anyone can chekcout this repository?I try to fix it but i faild and it took more than 10 hours.

@onlymash
Copy link

onlymash commented Aug 13, 2019

自动滚动的原因大概明白了。在 progressbar 也就是 footer 被移除前,添加了新的item,recyclerview 会尽可能的维持当前可见的progressbar 继续可见,于是瞬间在 footer前面插入大量的item就会造成自动滚动的效果

@onlymash
Copy link

假如有一个header,便能避免自动滚动,因为在header 和 footer 之间插入新的item 时 recyclerview 会优先保证 header 的可见性,从而避免了自动滚动

@onlymash
Copy link

所以解决办法是先让footer消失再向适配器发射数据

@parcool
Copy link

parcool commented Aug 13, 2019

@onlymash 太感谢了。完美!!!!

//callback.onResult(this, null, 2) move it after code of `networkState changed`
networkState.postValue(NetworkState.LOADED)
initialLoad.postValue(NetworkState.LOADED)
callback.onResult(this, null, 2)

@valentinilk
Copy link

I think I had the same issue and solved it by removing all "networkState.postValue()" calls from the "loadInitial()" function. Additionally this gets rid of the overlaying loading indicators.

@dlam
Copy link
Contributor

dlam commented Feb 23, 2021

I believe this issue is obsolete now with the sample being rewritten in paging3 and having loadState as a built-in concept in the library.

If there are any questions about that, happy to answer, but closing this out.

@dlam dlam closed this as completed Feb 23, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

7 participants