javaandroidandroid-recyclerviewandroid-orientationonsaveinstancestate

How to retain the state of a recyclerview of list of custom objects?


Main Goal:-

I have a list of sports news. Each item contains a sport name and some info. Clicking on it will show the latest news regarding that particular sport. The user has the option to swipe to dismiss a news, if they don't want it in the list or they can also drag and drop it, for example, if they want to see some news on top of others. Each item in the list is represented programmatically as a Sport.java object.

I want to retain the state of the list upon device orientation changes.

What I've tried:-

For the list, I have an arraylist of sport objects (ArrayList). I learned that to save a list of custom objects, they objects themselves need to be Parcelable. For this, I implemented the Parcelable.java interface like this:

package com.example.android.materialme;

import android.os.Parcel;
import android.os.Parcelable;

import androidx.annotation.NonNull;

/**
 * Data model for each row of the RecyclerView.
 */
class Sport implements Parcelable {

    //Member variables representing the title and information about the sport
    private String title;
    private String info;
    private String detail;
    private final int imageResource;

    /**
     * Constructor for the Sport data model
     * @param title The name if the sport.
     * @param info Information about the sport.
     */
    Sport(String title, String info, String detail, int imageResource) {
        this.title = title;
        this.info = info;
        this.detail = detail;
        this.imageResource = imageResource;
    }

    protected Sport(@NonNull Parcel in) {
        title = in.readString();
        info = in.readString();
        detail = in.readString();
        imageResource = in.readInt();
    }

    public static final Creator<Sport> CREATOR = new Creator<Sport>() {
        @Override
        public Sport createFromParcel(Parcel in) {
            return new Sport(in);
        }

        @Override
        public Sport[] newArray(int size) {
            return new Sport[size];
        }
    };

    String getTitle() {
        return title;
    }
  
    String getInfo() {
        return info;
    }

    int getImageResource(){
        return imageResource;
    }

    String getDetail(){
        return detail;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeString(title);
        parcel.writeString(info);
        parcel.writeString(detail);
        parcel.writeInt(imageResource);
    }
}

and then I used

outState.putParcelableArrayList(KEY, sportsList);

but, this doesn't work. The screen is just blank upon rotating device.

I tried debugging the app and found that the arraylist was being passed correctly with the data intact, it's just that the app is not being able to display it for some reason.

Also, the implementation of the fab button is so that it resets the whole list to its initial condition upon click. The fab works normally but if the orientation is changed once, it stops working (app doesn't crash). Changing the orientation back also doesn't fix the fab. So, to get the list again for any other test, I have to rerun the entire app.

Complete Code:-

MainActivity.java

package com.example.android.materialme;

import android.content.res.TypedArray;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.google.android.material.floatingactionbutton.FloatingActionButton;

import java.util.ArrayList;
import java.util.Collections;

public class MainActivity extends AppCompatActivity {

    //Member variables
    private RecyclerView mRecyclerView;
    private ArrayList<Sport> mSportsData;
    private SportsAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FloatingActionButton fab = findViewById(R.id.fab);
        fab.setOnClickListener(view -> resetSports());

        //Initialize the RecyclerView
        mRecyclerView = (RecyclerView)findViewById(R.id.recyclerView);

        //Set the Layout Manager
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));

        //Initialize the ArrayLIst that will contain the data
        mSportsData = new ArrayList<>();

        //Initialize the adapter and set it ot the RecyclerView
        mAdapter = new SportsAdapter(this, mSportsData);
        mRecyclerView.setAdapter(mAdapter);

        initializeData(savedInstanceState);

        ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
                ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT | ItemTouchHelper.UP | ItemTouchHelper.DOWN,
                ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
            @Override
            public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
                int from = viewHolder.getAdapterPosition();
                int to = target.getAdapterPosition();

                Collections.swap(mSportsData, from, to);

                mAdapter.notifyItemMoved(from, to);
                return true;
            }

            @Override
            public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
                mSportsData.remove(viewHolder.getAdapterPosition());
                mAdapter.notifyItemRemoved(viewHolder.getAdapterPosition());
            }
        });
        helper.attachToRecyclerView(mRecyclerView);
    }

    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putParcelableArrayList("state", mSportsData);
    }

    /**
     * Method for initializing the sports data from resources.
     */
    private void initializeData(Bundle savedInstanceState) {
        if(savedInstanceState!=null){
            mSportsData.clear();
            mSportsData = savedInstanceState.getParcelableArrayList("state");
        } else {
            //Get the resources from the XML file
            String[] sportsList = getResources().getStringArray(R.array.sports_titles);
            String[] sportsInfo = getResources().getStringArray(R.array.sports_info);
            String[] sportsDetail = getResources().getStringArray(R.array.sports_detail);
            TypedArray sportsImageResource = getResources().obtainTypedArray(R.array.sports_images);

            //Clear the existing data (to avoid duplication)
            mSportsData.clear();

            //Create the ArrayList of Sports objects with the titles and information about each sport
            for (int i = 0; i < sportsList.length; i++) {
                mSportsData.add(new Sport(sportsList[i], sportsInfo[i], sportsDetail[i], sportsImageResource.getResourceId(i, 0)));
            }

            sportsImageResource.recycle();
        }
        //Notify the adapter of the change
        mAdapter.notifyDataSetChanged();
    }

    public void resetSports(){
        initializeData(null);
    }

}

App Images:-
#1 Initial List
#2 Changed List
(Card #2 for sport basketball is swiped)

Initial List Changed list after swiping a card

Orientation change to landscape:-
Landscape mode


Solution

  • Even though the question is 4 months old and you probably don't need the answer anymore:

    The problem is that you initialize the adapter with mSportsData, but reassign another value to the variable later in initializeData(). The ArrayList bound to the adapter is still the empty one it got initialized with.

    A way to solve it would be to initialize mSportsData with either a new ArrayList if savedInstanceState is null or else the saved value, and to call initializeData only if savedInstanceState is null. You can remove the argument and therefore the if from initalizeData() completely.

    // Initialize the ArrayList that will contain the data.
    if (savedInstanceState == null) {
        mSportsData = new ArrayList<>();
    } else {
        mSportsData = savedInstanceState.getParcelableArrayList("state");
    }
    
    //Initialize the adapter and set it ot the RecyclerView (nothing changed here)
    mAdapter = new SportsAdapter(this, mSportsData);
    mRecyclerView.setAdapter(mAdapter);
    
    if (savedInstanceState == null) {
            initializeData();
        }