Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to retain RecyclerView's position after Orientation change, while using Firebase & ChildEventListener?

I'm working on a simple APOD app that implements:

  • RecyclerView
  • CardView
  • Firebase
  • Picasso

The app grabs images and text from Firebase and Firebase Storage, displays them in a CardView, and sets an OnClickListener to each View. When the user clicks on an image, I open a new Activity through an Intent. The second Activity displays the original clicked image, and more info about it.

I've implemented this all using a GridLayoutManager, 1 column if the user's phone is VERTICAL, 3 columns if the user's phone is HORIZONTAL.

The problem I'm having is I can't seem to save the RecyclerView's position on orientation change. I've tried every single option that I could find, but none seem to work. The only conclusion I could come up with, is that on rotation, I'm destroying Firebase's ChildEventListener to avoid a memory leak, and once orientation is complete, Firebase re-queries the database because of the new instance of ChildEventListener.

Is there any way I can save my RecyclerView's position on orientation change? I do not want android:configChanges as it won't let me change my layout, and I've already tried saving as a Parcelable, which was unsuccessful. I'm sure it's something easy I'm missing, but hey, I'm new to developing. Any help or suggestions on my code is greatly appreciated. Thanks!

Below are my classes, which I have shortened only to the necessary code.

MainActivity

public class MainActivity extends AppCompatActivity {  private RecyclerAdapter mRecyclerAdapter; private DatabaseReference mDatabaseReference; private RecyclerView mRecyclerView; private Query query;   @Override protected void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);      PreferenceManager.setDefaultValues(this, R.xml.preferences, false);     setContentView(R.layout.activity_main);     getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));      Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);     setSupportActionBar(toolbar);      final int columns = getResources().getInteger(R.integer.gallery_columns);      mDatabaseReference = FirebaseDatabase.getInstance().getReference();     query = mDatabaseReference.child("apod").orderByChild("date");      mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);     mRecyclerView.setHasFixedSize(true);     mRecyclerView.setLayoutManager(new GridLayoutManager(this, columns));       mRecyclerAdapter = new RecyclerAdapter(this, query);     mRecyclerView.setAdapter(mRecyclerAdapter);     }  @Override public void onDestroy() {     mRecyclerAdapter.cleanupListener();   }  } 

RecyclerAdapter

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ApodViewHolder> {  private final Context mContext; private final ChildEventListener mChildEventListener; private final Query mDatabaseReference; private final List<String> apodListIds = new ArrayList<>(); private final List<Apod> apodList = new ArrayList<>();  public RecyclerAdapter(final Context context, Query ref) {     mContext = context;     mDatabaseReference = ref;       ChildEventListener childEventListener = new ChildEventListener() {           @Override         public void onChildAdded(DataSnapshot dataSnapshot, String s) {             int oldListSize = getItemCount();             Apod apod = dataSnapshot.getValue(Apod.class);              //Add data and IDs to the list             apodListIds.add(dataSnapshot.getKey());             apodList.add(apod);              //Update the RecyclerView             notifyItemInserted(oldListSize - getItemCount() - 1);         }          @Override         public void onChildChanged(DataSnapshot dataSnapshot, String s) {          }          @Override         public void onChildRemoved(DataSnapshot dataSnapshot) {             String apodKey = dataSnapshot.getKey();             int apodIndex = apodListIds.indexOf(apodKey);              if (apodIndex > -1) {                  // Remove data and IDs from the list                 apodListIds.remove(apodIndex);                 apodList.remove(apodIndex);                  // Update the RecyclerView                 notifyDataSetChanged();             }         }          @Override         public void onChildMoved(DataSnapshot dataSnapshot, String s) {          }          @Override         public void onCancelled(DatabaseError databaseError) {          }      };     ref.addChildEventListener(childEventListener);     mChildEventListener = childEventListener;   }     @Override     public int getItemCount() {     return apodList.size(); }     public void cleanupListener() {     if (mChildEventListener != null) {         mDatabaseReference.removeEventListener(mChildEventListener);     }   }  } 
like image 366
Drew Szurko Avatar asked Feb 28 '17 16:02

Drew Szurko


People also ask

How do you change orientation when retaining data?

Another most common solution to dealing with orientation changes by setting the android:configChanges flag on your Activity in AndroidManifest. xml. Using this attribute your Activities won't be recreated and all your views and data will still be there after orientation change.

What happens when orientation changes android?

When you rotate your device and the screen changes orientation, Android usually destroys your application's existing Activities and Fragments and recreates them. Android does this so that your application can reload resources based on the new configuration.


2 Answers

While rotation activity get destroyed and re-created once again that means all your objects destroyed and re-created and also layout re-drawn again.

It you want to stop re-creation of activity you can use android:configChanges but it is bad practice and also not the right way.

Only thing remains now is to save position of recyclerview before rotation and get it after activity re-created.

// to save recyclerview position

 @Override     public void onSaveInstanceState(Bundle savedInstanceState) {       // Save UI state changes to the savedInstanceState.       // This bundle will be passed to onCreate if the process is       // killed and restarted.       savedInstanceState.putInt("position", mRecyclerView.getAdapterPosition()); // get current recycle view position here.       super.onSaveInstanceState(savedInstanceState);     } 

// to restore value

    @Override protected void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);      PreferenceManager.setDefaultValues(this, R.xml.preferences, false);     setContentView(R.layout.activity_main);     getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));      Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);     setSupportActionBar(toolbar);      final int columns = getResources().getInteger(R.integer.gallery_columns);      mDatabaseReference = FirebaseDatabase.getInstance().getReference();     query = mDatabaseReference.child("apod").orderByChild("date");      mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);     mRecyclerView.setHasFixedSize(true);     mRecyclerView.setLayoutManager(new GridLayoutManager(this, columns));       mRecyclerAdapter = new RecyclerAdapter(this, query);     mRecyclerView.setAdapter(mRecyclerAdapter);    if(savedInstanceState != null){      // scroll to existing position which exist before rotation.      mRecyclerView.scrollToPosition(savedInstanceState.getInt("position"));    }      } 

Hope these help you.

like image 73
Jitesh Mohite Avatar answered Sep 22 '22 13:09

Jitesh Mohite


EDIT:

I was finally able to make this work using multiple factors. If any one of these were left out, it simply would not work.

Create new GridLayoutManager in my onCreate

gridLayoutManager = new GridLayoutManager(getApplicationContext(), columns); 

Save the RecyclerView state in onPause

private final String KEY_RECYCLER_STATE = "recycler_state";  private static Bundle mBundleRecyclerViewState; private Parcelable mListState = null;  @Override protected void onPause() {     super.onPause();      mBundleRecyclerViewState = new Bundle();     mListState = mRecyclerView.getLayoutManager().onSaveInstanceState();     mBundleRecyclerViewState.putParcelable(KEY_RECYCLER_STATE, mListState); } 

Include configChanges in the AndroidManifest.xml

Saving and restoring the RecyclerView state was not enough. I also had to go against the grain and change my AndroidManifest.xml to 'android:configChanges="orientation|screenSize"'

<activity     android:name=".MainActivity"     android:configChanges="orientation|screenSize"> 

Restore the RecyclerView state in onConfigurationChanged using a Handler

The handler was one of the most important parts, if it's not included, it simply will not work and the position of the RecyclerView will reset to 0. I guess this has been a flaw going back a few years, and was never fixed. I then changed the GridLayoutManager's setSpanCount depending on the orientation.

@Override public void onConfigurationChanged(Configuration newConfig) {     super.onConfigurationChanged(newConfig);     columns = getResources().getInteger(R.integer.gallery_columns);      if (mBundleRecyclerViewState != null) {         new Handler().postDelayed(new Runnable() {             @Override             public void run() {                 mListState = mBundleRecyclerViewState.getParcelable(KEY_RECYCLER_STATE);                 mRecyclerView.getLayoutManager().onRestoreInstanceState(mListState);             }         }, 50);     }      // Checks the orientation of the screen     if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {         gridLayoutManager.setSpanCount(columns);     } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {         gridLayoutManager.setSpanCount(columns);     }     mRecyclerView.setLayoutManager(gridLayoutManager); } 

Like I said, all of these changes needed to be included, at least for me. I don't know if this is the most efficient way, but after spending many hours, it works for me.

like image 24
Drew Szurko Avatar answered Sep 21 '22 13:09

Drew Szurko