Search code examples
androidandroid-recyclerviewadapterlandscapenotifydatasetchanged

RecyclerView.notfiyDataSetChanged() not working in landscape layout


I am following along with the Android Developer CodeLab (link). When flipping the device to landscape, I cannot get notifyDataSetChanged() or notifyItemRangeInserted() to update my adapter. I implemented Parcelable on my object and it is correctly restoring my ArrayList after pulling it from the onSaveInstanceState bundle. But when I debug into these RecyclerView methods, the adapter is not populating with the ArrayList. Also, I noticed in landscape mode, the FAB, which resets the ArrayList to the default 11 items, also doesn't work when device is flipped to landscape. This leads me to believe something may be in the wrong class.

MainActivity

public class MainActivity extends AppCompatActivity {

// Tag for key in instance save bundle
static final String SPORT_ARRAY = "Sport Array";

// 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);

    // Initialize the RecyclerView
    mRecyclerView = 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 to the RecyclerView.
    mAdapter = new SportsAdapter(this, mSportsData);
    mRecyclerView.setAdapter(mAdapter);

    // Get the data or restore it
    if (savedInstanceState == null || !savedInstanceState.containsKey(SPORT_ARRAY)) {
        initializeData();
    } else {
        restoreData(savedInstanceState);
    }

    // Helper class for creating swipe to dismiss and drag and drop functionality.
    ItemTouchHelper helper = new ItemTouchHelper(
            new ItemTouchHelper.SimpleCallback(ItemTouchHelper.LEFT |
                    ItemTouchHelper.RIGHT | ItemTouchHelper.DOWN | ItemTouchHelper.UP,
                    ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
                /**
                 * Defines the drag and drop functionality.
                 *
                 * @param recyclerView The RecyclerView that contains the list items
                 * @param viewHolder The SportsViewHolder that is being moved.
                 * @param target The SportsViewHolder that you are switching the original one
                 *               with.
                 * @return true if the item was removed, false otherwise.
                 */
        @Override
        public boolean onMove(@NonNull RecyclerView recyclerView,
                              @NonNull RecyclerView.ViewHolder viewHolder,
                              @NonNull RecyclerView.ViewHolder target) {
            // Get the from and to positions.
            int from = viewHolder.getAdapterPosition();
            int to = target.getAdapterPosition();

            // Swap the items and notify the adapter.
            Collections.swap(mSportsData, from, to);
            mAdapter.notifyItemMoved(from, to);
            return true;
        }

                /**
                 * Defines the swipe to dismiss functionality.
                 * @param viewHolder The ViewHolder being swiped.
                 * @param direction The direction it is swiped in.
                 */
        @Override
        public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
            mSportsData.remove(viewHolder.getAdapterPosition());
            mAdapter.notifyItemRemoved(viewHolder.getAdapterPosition());
        }
    });

    // Attach the helper to the RecyclerView.
    helper.attachToRecyclerView(mRecyclerView);
}

/**
 * Method for initializing the sports data from resources.
 */
private void initializeData() {

    // 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_text);
    TypedArray sportsImageResources = 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], sportsImageResources.getResourceId(i, 0)));
    }

    // Recycle the sports image resource array.
    sportsImageResources.recycle();

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

private void restoreData(Bundle savedBundle) {
    mSportsData.clear();
    mSportsData = savedBundle.getParcelableArrayList(SPORT_ARRAY);
    // Notify the adapter of inserted items
    mAdapter.notifyItemRangeInserted(0, mSportsData.size());
}

/**
 * onClick method for the FAB that resets the data.
 * @param view The button view that was clicked.
 */
public void resetSports(View view) {
    initializeData();
}

/**
 * Save the instance state
 * @param outState The current instance state
 */
@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putParcelableArrayList(SPORT_ARRAY, mSportsData);
    super.onSaveInstanceState(outState);
}
}

SportsAdapter

public class SportsAdapter extends RecyclerView.Adapter<SportsAdapter.ViewHolder>{

    // Member variables
    private ArrayList<Sport> mSportsData;
    private Context mContext;

    /**
     * Constructor that passes in the sports data and the context.
     * @param context Context of the application.
     * @param sportsData ArrayList containing the sports data.
     */
    SportsAdapter(Context context, ArrayList<Sport> sportsData) {
        this.mSportsData = sportsData;
        this.mContext = context;
    }

    /**
     * Required method for creating the ViewHolder objects.
     * @param viewGroup The ViewGroup into which the View will be added after it is bound to an
     *                  adapter position.
     * @param i The view type of the new View.
     * @return The newly created ViewHolder.
     */
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        return new ViewHolder(LayoutInflater.from(mContext).inflate(R.layout.list_item, viewGroup,
                false));
    }

    /**
     * Required method that binds the data to the ViewHolder.
     * @param viewHolder The ViewHolder into which the data should be put.
     * @param i The adapter position.
     */
    @Override
    public void onBindViewHolder(@NonNull SportsAdapter.ViewHolder viewHolder, int i) {
        // Get the current sport.
        Sport currentSport = mSportsData.get(i);
        // Populate the TextViews with data.
        viewHolder.bindTo(currentSport);
    }

    /**
     * Required method for determining the size of the data set.
     * @return Size of the data set.
     */
    @Override
    public int getItemCount() {
        return mSportsData.size();
    }

    /**
     * ViewHolder class that represents each row of data in the RecyclerVIew.
     */
    class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{

        // Member variables for the TextViews.
        private TextView mTitleText;
        private TextView mInfoText;
        private ImageView mSportsImage;

        /**
         * Constructor for the ViewHolder, used in onCreateViewHolder().
         * @param itemView The rootview of the list_item.xml layout file.
         */
        ViewHolder(View itemView) {
            super(itemView);

            // Initialize the views
            mTitleText = itemView.findViewById(R.id.title);
            mInfoText = itemView.findViewById(R.id.subTitle);
            mSportsImage = itemView.findViewById(R.id.sportsImage);

            // Set the OnClickListener to the entire view.
            itemView.setOnClickListener(this);
        }

        void bindTo(Sport currentSport) {
            // Populate the TextViews with data.
            mTitleText.setText(currentSport.getTitle());
            mInfoText.setText(currentSport.getInfo());
            Glide.with(mContext).load(currentSport.getImageResource()).into(mSportsImage);
        }

        /**
         * Handle click to show DetailActivity.
         *
         * @param v The view that was clicked.
         */
        @Override
        public void onClick(View v) {
            Sport currentSport = mSportsData.get(getAdapterPosition());
            Intent detailIntent = new Intent(mContext, DetailActivity.class);
            detailIntent.putExtra("title", currentSport.getTitle());
            detailIntent.putExtra("detail", currentSport.getDetail());
            detailIntent.putExtra("image_resource", currentSport.getImageResource());
            mContext.startActivity(detailIntent);
        }
    }
}

Sport Class

public class Sport implements Parcelable {

    // Member variables representing the title and information about the sport.
    private String mTitle;
    private String mInfo;
    private String mDetail;
    private final int mImageResource;

    /**
     * Constructor for the Sport data model.
     * @param title The name of the sport.
     * @param info Information about the sport.
     * @param detail Details about the sport (from Wikipedia).
     * @param imageResource The image resource.
     */
    Sport(String title, String info, String detail, int imageResource) {
        this.mTitle = title;
        this.mInfo = info;
        this.mDetail = detail;
        this.mImageResource = imageResource;
    }

    protected Sport(Parcel in) {
        mTitle = in.readString();
        mInfo = in.readString();
        mDetail = in.readString();
        mImageResource = 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];
        }
    };

    /**
     * Gets the title of the sport.
     * @return The title of the sport.
     */
    String getTitle() {
        return mTitle;
    }

    /**
     * Gets the info about the sport.
     * @return The info about the sport.
     */
    String getInfo() {
        return mInfo;
    }

    /**
     * Gets the details about the sport (from Wikipedia).
     * @return The details about the sport.
     */
    String getDetail() {
        return mDetail;
    }

    /**
     * Gets the image resource.
     * @return The image resource.
     */
    public int getImageResource() {
        return mImageResource;
    }

    /**
     * Describe the kinds of special objects contained in this Parcelable
     * instance's marshaled representation. For example, if the object will
     * include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
     * the return value of this method must include the
     * {@link #CONTENTS_FILE_DESCRIPTOR} bit.
     *
     * @return a bitmask indicating the set of special object types marshaled
     * by this Parcelable object instance.
     */
    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * Flatten this object in to a Parcel.
     *
     * @param dest  The Parcel in which the object should be written.
     * @param flags Additional flags about how the object should be written.
     *              May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mTitle);
        dest.writeString(mInfo);
        dest.writeString(mDetail);
        dest.writeInt(mImageResource);
    }
}

Solution

  • You're not updating the adapter properly. When it's time to restore the list from the saved Bundle in the restoreData() method you do:

    mSportsData = savedBundle.getParcelableArrayList(SPORT_ARRAY);
    

    This simply changes the mSportsData reference to point to the list from the Bundle. However, the RecyclerView's adapter will be based on the empty list that you initially created in onCreate() with:

    mSportsData = new ArrayList<>();
    

    This is why your FAB also fails because it calls initializeData() on a list which is not used by the adapter.

    The proper way to update the adapter is to put the objects from the list in the Bundle in mSportsData:

    mSportsData.addAll(savedBundle.getParcelableArrayList(SPORT_ARRAY));