How to maintain scroll position of FirebaseRecyclerAdapter

Viewed 668

I am trying to maintain the scroll position of my FirebaseRecyclerAdapter when I am returning to the same activity from another activity.

Here is my function which sets the FirebaseRecyclerAdapter

public void setTheScreen(){
   FirebaseRecyclerAdapter<Post,PostViewHolder> firebaseRecyclerAdapter = new FirebaseRecyclerAdapter<Post,PostViewHolder>(
           Post.class,
           R.layout.post_display_blueprint,
           PostViewHolder.class,
           postsNode
   ) {
       @Override
       public void onDataChanged() {
           if(dialog != null && dialog.isShowing()){
               dialog.dismiss();
           }
       }

       @Override
       protected void populateViewHolder(final PostViewHolder viewHolder, Post model, int position) {
           final String key = getRef(position).getKey();
           mRefPosts = FirebaseDatabase.getInstance().getReference().child("posts").child(key);
           mLoveNode = mRef.child("loves");
           mRefLoves = mRef.child("loves").child(key);
           viewHolder.setTitle(model.getTitle());
           viewHolder.setLoves(mRefLoves);
           viewHolder.setLoveButton(Uid,key);
           viewHolder.setImage(getApplicationContext(),model.getImageUrl());


           mRefPosts.addValueEventListener(new ValueEventListener() {
               @Override
               public void onDataChange(DataSnapshot dataSnapshot) {

                   String isAnonymous = dataSnapshot.child("anonymous").getValue(String.class);
                   if (isAnonymous != null && isAnonymous.equals("true")) {
                       viewHolder.anonymousButton.setVisibility(View.VISIBLE);
                   }

               }
               @Override
               public void onCancelled(DatabaseError databaseError) {

               }
           });


            viewHolder.mView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {

                    Intent intent = new Intent(MainActivity.this,PostDisplayPageActivity.class);
                    intent.putExtra("Key",key);
                    startActivity(intent);

                }
            });
       }
   };
   mRecyclerView.setAdapter(firebaseRecyclerAdapter);
    firebaseRecyclerAdapter.notifyDataSetChanged();

}

By clicking on any view created by the adapter I am going to a new activity named PostDisplayPageActivity from my MainActivity and on back pressing I am finishing the PostDisplayPageActivity and coming back to MainActivity.

When I come back to my MainActivity the list starts from the 0 position

I used the following code in my onPause and onResume methods, but it is not working.

 @Override
protected void onPause() {
    super.onPause();

    currentVisiblePosition = ((LinearLayoutManager)mRecyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
    Toast.makeText(MainActivity.this,String.valueOf(currentVisiblePosition),Toast.LENGTH_SHORT).show();

}

@Override
protected void onResume() {
    super.onResume();

    setTheScreen();
    mRecyclerView.getLayoutManager().scrollToPosition(currentVisiblePosition);
    Toast.makeText(MainActivity.this,String.valueOf(currentVisiblePosition),Toast.LENGTH_SHORT).show();
    currentVisiblePosition = 0;


}

From those 2 Toasts I am getting the correct positions but the view always starts from position 0.

for my RecyclerView I am using LinearLayoutManager as mRecyclerView.setLayoutManager(mLinearLayoutManager);

Someone please tell me what am I doing wrong or suggest me a different way to do this.

2 Answers

I solved my problem with a simpler solution. I just called adapter.startListening(); in onViewCreated() instead of onStart and called adapter.stopListening(); in onDestroyView() instead of onStop() That prevented the entire list from regenerating while coming back from next activity and thus retained the scroll position where it was previously.

Having faced the same problem, I found numerous suggestions that did not work for me. So, I came up with a workaround for this issue of restoring the Firebase RecyclerView state/scroll position.

Keep a field of the instance state of the RecyclerView instanceStateFirebaseRecycler in the Activity/Fragment where the Firebase RecyclerView is implemented:

private Parcelable instanceStateFirebaseRecycler;

First, save the Firebase RecyclerView's instance state instanceStateFirebaseRecycler, which keeps the scroll position information, onPause():

@Override
public void onPause() {
    saveScrollPosition();
    super.onPause();
}

where the saveScrollPosition() method is simply:

private void saveScrollPosition(){
    instanceStateFirebaseRecycler = firebaseRecyclerView.getLayoutManager().onSaveInstanceState();
}

Next is the tricky part, you add a listener to your Firebase Adapter that notifies you when the adapter is ready, that is, when the getItemCount() is not zero. In the listener's callback you restore the recycler's state/scroll position:

firebaseAdapter = new FirebaseAdapter(options, new FirebaseAdapter.OnRestoreRecyclerStateListener() {
        @Override
        public void onRestoreRecyclerState() {
            restoreScrollPosition();
        }
    });
    firebaseRecyclerView.setAdapter(firebaseAdapter);

where the custom interface is just:

public interface OnRestoreRecyclerStateListener {
    void onRestoreRecyclerState();
}

and the callback is triggered in the Firebase Adapter onBindViewHolder (I did't find the better place) to make sure the adapter items are ready:

@Override
protected void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull MyModel model) {
    ...
    onRestoreRecyclerStateListener.onRestoreRecyclerState();
}

, and where the restoreScrollPosition() method is:

private void restoreScrollPosition(){
    if (instanceStateFirebaseRecycler == null) return;
    firebaseRecyclerView.getRootView().setVisibility(View.GONE);
    final Handler handler = new Handler(Looper.getMainLooper());
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            if (instanceStateFirebaseRecycler == null) return;
            firebaseRecyclerView.getLayoutManager().onRestoreInstanceState(instanceStateFirebaseRecycler);
            instanceStateFirebaseRecycler = null;
            firebaseRecyclerView.getRootView().setVisibility(View.VISIBLE);
        }
    }, 100);
}

In the restoreScrollPosition() method I check for instanceStateFirebaseRecycler == null to avoid unnecessary runs. Also, I delay the instance state restoration by means of Handler, because the items are not drawn yet and will be shortly. If onRestoreInstanceState is called without delay, the state will not be restored as expected. Additionally, I toggle the RecyclerView's visibility by setVisibility() to hide ugly items reloading and fast scrolling that can be interpreted by the user as glitches.

By the way, the solution suggested by @Sanju looks a bit dangerous in terms of traffic and battery efficiency, since a user can put a running app in the background for a long time with the listener being active for all this time.

Related