Advanced RecyclerView

RecyclerView is the “Swiss army knife” of Android selection widgets. You can use it for a wide range of scenarios, well beyond what classic AdapterView widgets — like ListView or GridView — could handle.

In this chapter, we will “go outside the (AdapterView) box” and explore some advanced uses of RecyclerView.

Prerequisites

Understanding this chapter requires that you have read the preceding chapter, on RecyclerView.

RecyclerView as Pager

ViewPager has been used for horizontally-swiped page-at-a-time user interfaces since its debut in 2011.

However, ViewPager is not that flexible:

And so on.

However, as it turns out, RecyclerView can be readily adapted to serve as a ViewPager replacement. Instead of a PagerAdapter, you use an ordinary RecyclerView.Adapter, where your pages are simple views. RecyclerView itself is far more flexible than is ViewPager, giving you a stronger foundation for more advanced paging scenarios.

Using RecyclerViewPager

The original solution for using RecyclerView as a ViewPager replacement came in the form of a third-party library, com.github.lsjwzh.RecyclerViewPager. This library offers a a RecyclerViewPager subclass of RecyclerView that offers the page-at-a-time swiping metaphor.

The RecyclerViewPager/PlainRVP sample project illustrates its use. This is another rendition of the 10-EditText-widgets pager that was used in the chapter on ViewPager, swapping in RecyclerViewPager for the ViewPager.

Adding the Dependency

The com.github.lsjwzh.RecyclerViewPager library is not on JCenter or Maven Central. Instead, it is on jitpack.io, an artifact repository that builds artifacts directly from GitHub source repositories. So, to use this library, we need to add jitpack.io as a repository, in addition to adding the RecyclerViewPager library itself:

apply plugin: 'com.android.application'

repositories {
  maven { url "https://jitpack.io" }
}

dependencies {
    compile 'com.android.support:recyclerview-v7:25.1.0'
    compile 'com.github.lsjwzh.RecyclerViewPager:lib:v1.1.2'
}

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 25
    }
}

(from RecyclerViewPager/PlainRVP/app/build.gradle)

Using the Widget

In the equivalent ViewPager sample app, the main.xml layout resource held the ViewPager. In this sample, it holds the RecyclerViewPager (or, more accurately, the com.lsjwzh.widget.recyclerviewpager.RecyclerViewPager, since the class name needs to be fully-qualified since it is coming from a library):

<?xml version="1.0" encoding="utf-8"?>
<com.lsjwzh.widget.recyclerviewpager.RecyclerViewPager android:id="@+id/pager"
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:clipToPadding="false"
  android:layout_margin="@dimen/pager_padding"
  app:rvp_singlePageFling="true"
  app:rvp_triggerOffset="0.1" />

(from RecyclerViewPager/PlainRVP/app/src/main/res/layout/main.xml)

The app:rvp_singlePageFling indicates that we want to limit the user to switch one page at a time, rather than a long fling gesture resulting in moving through many pages at once. The app:rvp_triggerOffset attribute is undocumented but appears to control how much of a swipe gesture is necessary to trigger a page change.

Populating the Pages

With ViewPager, you supply the pages via a PagerAdapter, typically a FragmentPagerAdapter or a FragmentStatePagerAdapter. With RecyclerViewPager, you supply the pages via a RecyclerView.Adapter, just as you would with any other RecyclerView.

So, in onCreate() of the MainActivity, we get the RecyclerViewPager, hand it a horizontal LinearLayoutManager, create a PageAdapter, and attach that PageAdapter to the RecyclerViewPager:

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    RecyclerViewPager pager=(RecyclerViewPager)findViewById(R.id.pager);

    pager.setLayoutManager(new LinearLayoutManager(this,
      LinearLayoutManager.HORIZONTAL, false));
    adapter=new PageAdapter(pager, getLayoutInflater());
    pager.setAdapter(adapter);
  }

(from RecyclerViewPager/PlainRVP/app/src/main/java/com/commonsware/android/rvp/MainActivity.java)

By using a horizontal LinearLayoutManager, the RecyclerViewPager will behave akin to a regular ViewPager, with navigation occurring via horizontal swipes. Want a vertical ViewPager? Replace the horizontal LinearLayoutManager with a vertical one, and you are set.

Our PageAdapter is a RecyclerView.Adapter, for a RecyclerView.ViewHolder named PageController. The basic setup for PageAdapter is not that different than any other RecyclerView.Adapter:

class PageAdapter extends RecyclerView.Adapter<PageController> {
  private static final String STATE_BUFFERS="buffers";
  private static final int PAGE_COUNT=10;
  private final RecyclerViewPager pager;
  private final LayoutInflater inflater;
  private ArrayList<String> buffers=new ArrayList<>();

  PageAdapter(RecyclerViewPager pager, LayoutInflater inflater) {
    this.pager=pager;
    this.inflater=inflater;

    for (int i=0;i<10;i++) {
      buffers.add("");
    }
  }

  @Override
  public PageController onCreateViewHolder(ViewGroup parent, int viewType) {
    return(new PageController(inflater.inflate(R.layout.editor, parent, false)));
  }

  @Override
  public void onBindViewHolder(PageController holder, int position) {
    holder.setText(buffers.get(position));
  }

  @Override
  public int getItemCount() {
    return(PAGE_COUNT);
  }


(from RecyclerViewPager/PlainRVP/app/src/main/java/com/commonsware/android/rvp/PageAdapter.java)

In this case, our model data is an ArrayList of String objects, representing the text that the user enters into each page’s EditText. PAGE_COUNT caps the number of editors (and pages) at 10, and so we initialize 10 buffers in the PageAdapter constructor.

The layout used for the pages — inflated by onCreateViewHolder() – is just a full-page multi-line EditText widget:

<EditText xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/editor"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:inputType="textMultiLine"
  android:gravity="left|top"
  />
(from RecyclerViewPager/PlainRVP/app/src/main/res/layout/editor.xml)

PageController is a fairly basic RecyclerView.ViewHolder, wrapping our EditText and offering a getter and setter for manipulating the text in the editor:

package com.commonsware.android.rvp;

import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.EditText;

class PageController extends RecyclerView.ViewHolder {
  private final EditText editor;

  PageController(View itemView) {
    super(itemView);

    editor=(EditText)itemView.findViewById(R.id.editor);
  }

  void setText(String text) {
    editor.setText(text);
    editor.setHint(editor.getContext().getString(R.string.hint,
      getAdapterPosition()+1));
  }

  String getText() {
    return(editor.getText().toString());
  }
}

(from RecyclerViewPager/PlainRVP/app/src/main/java/com/commonsware/android/rvp/PageController.java)

In case the buffer is empty (as it is at the outset), we also set the hint of the EditText to be the current page’s index, adding one to adjust the range to start at 1 rather than 0. The hint text itself is in a string resource, with a %d placeholder for the page number:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="app_name">RVP Demo</string>
  <string name="hint">Editor #%d</string>

</resources>
(from RecyclerViewPager/PlainRVP/app/src/main/res/values/strings.xml)

We use the N-parameter getString() method to not only retrieve the hint string resource but run it through String.format() to populate the placeholders, in this case using getAdapterPosition() to determine our page number.

Dealing with Recycling

RecyclerView wants to recycle its items. That is in contrast to how the stock PagerAdapter implementation work:

If our pages were read-only, we would not have to worry about recycling. This is how many RecyclerView implementations work — they just focus on binding the right data into the right RecyclerView.ViewHolder at the right time, based on calls to onBindViewHolder in the RecyclerView.Adapter.

However, when the RecyclerView items are interactive, we need to make sure that we hold onto the changed data, rather than having it be overwritten when we bind fresh data into the recycled item’s views.

In PageAdapter, we handle this by overriding onViewDetachedFromWindow(), which is called when the views of a PageController are no longer part of our activity’s window. Typically, this will occur as part of scrolling. In our case, we use this opportunity to grab the current contents of that EditText widget and update our buffers data model to match:

  @Override
  public void onViewDetachedFromWindow(PageController holder) {
    super.onViewDetachedFromWindow(holder);

    buffers.set(holder.getAdapterPosition(), holder.getText());
  }

(from RecyclerViewPager/PlainRVP/app/src/main/java/com/commonsware/android/rvp/PageAdapter.java)

Alternatively, you could aim to deal with this more in “real time”, such as by using a TextWatcher to update the model as the user types. That adds a fair bit of overhead, though.

Dealing with Configuration Changes

We need to make sure that we do not lose what the user types into the pages when we undergo a configuration change. Since our model is a simple ArrayList of String objects, we can use the saved instance state Bundle to hold onto the in-flight information.

A RecyclerView.Adapter does not have its own onSaveInstanceState() method, but we can add one, then call it from MainActivity:

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

    Bundle adapterState=new Bundle();

    adapter.onSaveInstanceState(adapterState);
    state.putBundle(STATE_ADAPTER, adapterState);
  }

(from RecyclerViewPager/PlainRVP/app/src/main/java/com/commonsware/android/rvp/MainActivity.java)

Here, MainActivity provides a fresh Bundle to the adapter. This way, values that the adapter wishes to save in the instance state will not collide with anything else the activity would want to save in the instance state, due to accidental key collisions. In this case, this may well be superfluous, but it is a worthwhile practice.

The challenge in our PageAdapter is that buffers only has text from those PageController objects that have been recycled. That will not include the currently-visible page or possibly some adjacent pages.

So, we iterate over all pages and call findViewHolderForAdapterPosition() on the RecyclerView itself. This will return null for any positions for which no PageController is presently allocated, or the PageController for the position for those positions that are actively being used. For those latter ones, we update the buffers to reflect whatever is in the EditText widgets, saving that into the instance state Bundle:

  void onSaveInstanceState(Bundle state) {
    for (int i=0;i<PAGE_COUNT;i++) {
      PageController holder=
        (PageController)pager.findViewHolderForAdapterPosition(i);

      if (holder!=null) {
        buffers.set(i, holder.getText());
      }
    }

    state.putStringArrayList(STATE_BUFFERS, buffers);
  }

(from RecyclerViewPager/PlainRVP/app/src/main/java/com/commonsware/android/rvp/PageAdapter.java)

MainActivity has a corresponding onRestoreInstanceState() method:

  @Override
  protected void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);

    adapter.onRestoreInstanceState(state.getBundle(STATE_ADAPTER));
  }

(from RecyclerViewPager/PlainRVP/app/src/main/java/com/commonsware/android/rvp/MainActivity.java)

That delegates the work to the onRestoreInstanceState() method on the PageAdapter:

  void onRestoreInstanceState(Bundle state) {
    buffers=state.getStringArrayList(STATE_BUFFERS);
  }

(from RecyclerViewPager/PlainRVP/app/src/main/java/com/commonsware/android/rvp/PageAdapter.java)

This sets up our buffers for use in populating pages again.

Using SnapHelper

RecyclerViewPager was first released in 2014. Since then, RecyclerView and its supporting classes have evolved. Now, you can get much of the functionality of RecyclerViewPager with an ordinary RecyclerView, with the assistance of SnapHelper. As Lisa Wray profiled in a droidcon NYC 2016 presentation, SnapHelper is a utility class that forces swipe gestures to “snap” to certain locations or boundaries. And, there is a PagerSnapHelper that, in conjunction with properly-configured RecyclerView and items, gives you ViewPager-like behavior.

The RecyclerViewPager/PlainSnap sample project is a clone of the PlainRVP sample, except that the RecyclerViewPager is replaced by a RecyclerView and a PagerSnapHelper.

There are three requirements of PagerSnapHelper. Two are tied to the layouts: both the RecyclerView and its items need to have match_parent for android:layout_width and android:layout_height. That was how PlainRVP was set up already, though PlainSnap swaps in a RecyclerView for the RecyclerViewPager in main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView android:id="@+id/pager"
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:layout_margin="@dimen/pager_padding"
  android:clipToPadding="false" />

(from RecyclerViewPager/PlainSnap/app/src/main/res/layout/main.xml)

The other requirement is that we create an instance of PagerSnapHelper and call attachToRecyclerView() on it, supplying our RecyclerView. This is handled in an updated MainActivity:

package com.commonsware.android.rvp;

import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.PagerSnapHelper;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SnapHelper;

public class MainActivity extends Activity {
  private static final String STATE_ADAPTER="adapter";
  private final SnapHelper snapperCarr=new PagerSnapHelper();
  private PageAdapter adapter;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    RecyclerView pager=(RecyclerView)findViewById(R.id.pager);

    pager.setLayoutManager(new LinearLayoutManager(this,
      LinearLayoutManager.HORIZONTAL, false));
    snapperCarr.attachToRecyclerView(pager);

    adapter=new PageAdapter(pager, getLayoutInflater());
    pager.setAdapter(adapter);
  }

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

    Bundle adapterState=new Bundle();

    adapter.onSaveInstanceState(adapterState);
    state.putBundle(STATE_ADAPTER, adapterState);
  }

  @Override
  protected void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);

    adapter.onRestoreInstanceState(state.getBundle(STATE_ADAPTER));
  }
}
(from RecyclerViewPager/PlainSnap/app/src/main/java/com/commonsware/android/rvp/MainActivity.java)

We hold onto the PagerSnapHelper in a field, to ensure that it will not be garbage-collected unexpectedly. Probably the PagerSnapHelper has sufficient connections to the RecyclerView to ensure that it will stay around as long as its associated RecyclerView does, but that is not apparent from the API or the documentation.

Beyond that, we configure the RecyclerView much as we had configured the RecyclerViewPager, and our PageAdapter and PageController are largely unaffected by the UI switch. In the end, we wind up once again with page-at-a-time horizontal swiping, though this time we can skip the third-party library.

Adding Tabs

Many times, with a pager-style interface, we want an indicator to help the user understand where they are within the range of pages offered by the pager. One of the more popular indicator styles is tabs, as those also provide an alternative navigation option, with the user tapping on tabs to switch to particular pages.

For adding tabs to a RecyclerView-powered pager, you need a tab implementation that is not tied inextricably to ViewPager, the way PagerTabStrip is. At the same time, you need one that is not tied inextricably to some other particular UI setup, the way that FragmentTabHost is. Instead, you need tabs that “stick to their knitting” and focus solely on handling the tab UI, giving you the hooks necessary to update your UI based on tab changes, and to update the tabs based on other UI changes.

TabLayout, from the Design Support library, is one such tab implementation. While it offers hooks into ViewPager, those are optional. You have two main options for using TabLayout:

  1. Literally use the version from the Design Support library, which will require you to use appcompat-v7. This works back to API Level 7, as does RecyclerView itself.
  2. Use the TabLayout from the CWAC-CrossPort library, which is the official TabLayout code, with all references to appcompat-v7 replaced by references to Theme.Material and related native items. However, this limits this cross-ported TabLayout to API Level 21 and higher (Android 5.0).

The RecyclerViewPager/TabSnap sample project is a clone of the PlainSnap sample, with tabs added, via the TabLayout from CWAC-CrossPort. As a result, we need the CWAC-CrossPort dependency and need to raise our minSdkVersion to 21:

apply plugin: 'com.android.application'

repositories {
  maven {
    url "https://s3.amazonaws.com/repo.commonsware.com"
  }
}

dependencies {
    compile 'com.android.support:recyclerview-v7:25.1.0'
    compile 'com.commonsware.cwac:crossport:0.0.2'
}

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 25
        applicationId 'com.commonsware.cwac.rvp.tabsnap'
    }
}

(from RecyclerViewPager/TabSnap/app/build.gradle)

The tabs themselves can then go above the RecyclerView, in a vertical LinearLayout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:orientation="vertical">

  <com.commonsware.cwac.crossport.design.widget.TabLayout
    android:id="@+id/tabs"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:tabMode="scrollable"/>

  <android.support.v7.widget.RecyclerView
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/pager_padding"
    android:clipToPadding="false" />
</LinearLayout>
(from RecyclerViewPager/TabSnap/app/src/main/res/layout/main.xml)

Next, we need to set up the tab contents in onCreate() of MainActivity. To do that, we get our hands on the TabLayout using findViewById(), then iterate through the items in the PageAdapter to set up tabs for each:

    final TabLayout tabs=(TabLayout)findViewById(R.id.tabs);

    for (int i=0;i<adapter.getItemCount();i++) {
      tabs.addTab(tabs.newTab().setText(adapter.getTabText(this, i)));
    }

(from RecyclerViewPager/TabSnap/app/src/main/java/com/commonsware/android/rvp/MainActivity.java)

We ask the PageAdapter for the text to show in the tab, via a getTabText() method:

  String getTabText(Context ctxt, int position) {
    return(PageController.getTitle(ctxt, position));
  }

(from RecyclerViewPager/TabSnap/app/src/main/java/com/commonsware/android/rvp/PageAdapter.java)

That, in turn, delegates to a static version of the getTitle() method on PageController, to fill in the string resource template with the proper page number:

  static String getTitle(Context ctxt, int position) {
    return(ctxt.getString(R.string.hint, position+1));
  }

(from RecyclerViewPager/TabSnap/app/src/main/java/com/commonsware/android/rvp/PageController.java)

We now need to add code in onCreate() of MainActivity to tie the navigation together:

This is handled by event listeners:

    tabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        pager.smoothScrollToPosition(tab.getPosition());
      }

      @Override
      public void onTabUnselected(TabLayout.Tab tab) {
        // unused
      }

      @Override
      public void onTabReselected(TabLayout.Tab tab) {
        // unused
      }
    });

    pager.setOnScrollListener(new RecyclerView.OnScrollListener() {
      @Override
      public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        int tab=layoutManager.findFirstCompletelyVisibleItemPosition();

        if (tab>=0 && tab<tabs.getTabCount()) {
          tabs.getTabAt(tab).select();
        }
      }
    });

(from RecyclerViewPager/TabSnap/app/src/main/java/com/commonsware/android/rvp/MainActivity.java)

When a tab is selected, our anonymous TabLayout.OnTabSelectedListener implementation will get control in onTabSelected(). There, we tell the RecyclerView to scroll to show a particular position, tied to the position of the selected tab.

Similarly, when the user scrolls the pager, we need to update the tabs to show the new selection. To do that, we take advantage of the findFirstCompletelyVisibleItemPosition() method on LinearLayoutManager. As the (lengthy) method name suggests, this returns the position of the first item that is completely visible within the pager. This might return –1, if we are in the middle of a swipe, as no item may be completely visible at that point. But, once we get a plausible value, we tell the TabLayout to select that tab.

RecyclerViewPager has a more sophisticated algorithm for integrating with the official Design Support library implementation of TabLayout. RecyclerViewPager calculates the position of the tab highlight, based upon the current swipe position, and updates that. This provides visual feedback within the tabs while the swipe is going on. The approach shown in the sample app has the effect of only updating the tabs once the swipe is completed.

Changing the Mix of Pages

A major problem with ViewPager — or, more accurately, the stock PagerAdapter implementations — was when we wanted to add, move, or remove pages on the fly, such as by the user tapping some sort of “add tab” action bar item. To pull this off required a custom PagerAdapter, as neither FragmentPagerAdapter nor FragmentStatePagerAdapter were well-suited for this scenario.

RecyclerView can readily handle such changes in its roster of items, and so we can add, move, and remove pages easily enough. However, it does require a somewhat more elaborate example, not so much due to RecyclerView, but for the rest of our business logic. For example, all the prior samples hard-coded getItemCount() to return 10 (by way of a PAGE_COUNT value), and that will no longer be the case as we add and remove pages.

The RecyclerViewPager/DynSnap sample project is a clone of the TabSnap sample, with four action bar items, to add, “split”, swap, and remove pages.

Using a Richer Model

In our previous samples, the tab title was tied to the tab position. That worked well, since our tabs all had fixed positions. Now, though, we want to add, move, and remove tabs, which means the positions of the existing tabs will change. We do not want to change the tab titles just because the positions change, as that will be very confusing for the user. So, our data model is more than just the text that the user types in — now it needs to include the title associated with that text.

To that end, DynSnap sets up an actual model object, called EditBuffer, to track both of those values:

package com.commonsware.android.rvp;

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

class EditBuffer implements Parcelable {
  private String prose;
  final private String title;

  EditBuffer(String title) {
    this(title, "");
  }

  EditBuffer(String title, String prose) {
    this.prose=prose;
    this.title=title;
  }

  protected EditBuffer(Parcel in) {
    prose=in.readString();
    title=in.readString();
  }

  @Override
  public String toString() {
    return(title);
  }

  String getProse() {
    return(prose);
  }

  void setProse(String prose) {
    this.prose=prose;
  }

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

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeString(prose);
    dest.writeString(title);
  }

  @SuppressWarnings("unused")
  public static final Parcelable.Creator<EditBuffer> CREATOR=
    new Parcelable.Creator<EditBuffer>() {
    @Override
    public EditBuffer createFromParcel(Parcel in) {
      return(new EditBuffer(in));
    }

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

(from RecyclerViewPager/DynSnap/app/src/main/java/com/commonsware/android/rvp/EditBuffer.java)

EditBuffer is Parcelable, so we will be able to hold onto this information in the saved instance state Bundle, for dealing with configuration changes.

The buffers field in PageAdapter now is an ArrayList of EditBuffer objects, and our original constructor and other methods are updated to match that:

  PageAdapter(RecyclerView pager, LayoutInflater inflater) {
    this.pager=pager;
    this.inflater=inflater;

    for (int i=0;i<10;i++) {
      buffers.add(new EditBuffer(getNextTitle()));
    }
  }

  @Override
  public PageController onCreateViewHolder(ViewGroup parent, int viewType) {
    return(new PageController(inflater.inflate(R.layout.editor, parent, false)));
  }

  @Override
  public void onBindViewHolder(PageController holder, int position) {
    holder.setText(buffers.get(position).getProse());
    holder.setTitle(buffers.get(position).toString());
  }

  @Override
  public int getItemCount() {
    return(buffers.size());
  }

  String getTabText(int position) {
    return(buffers.get(position).toString());
  }

  @Override
  public void onViewDetachedFromWindow(PageController holder) {
    super.onViewDetachedFromWindow(holder);

    if (holder.getAdapterPosition()>=0) {
      buffers.get(holder.getAdapterPosition()).setProse(holder.getText());
    }
  }

  private String getNextTitle() {
    return(pager.getContext().getString(R.string.hint, nextTabNumber++));
  }

  void onSaveInstanceState(Bundle state) {
    for (int i=0;i<getItemCount();i++) {
      updateProse(i);
    }

    state.putParcelableArrayList(STATE_BUFFERS, buffers);
  }

  void onRestoreInstanceState(Bundle state) {
    buffers=state.getParcelableArrayList(STATE_BUFFERS);
  }

(from RecyclerViewPager/DynSnap/app/src/main/java/com/commonsware/android/rvp/PageAdapter.java)

We pull out the “update-the-EditBuffer-based-on-the-holder-contents” logic into a separate updateProse() method, that we will reuse elsewhere:

  private void updateProse(int position) {
    PageController holder=
      (PageController)pager.findViewHolderForAdapterPosition(position);

    if (holder!=null) {
      buffers.get(position).setProse(holder.getText());
    }
  }

(from RecyclerViewPager/DynSnap/app/src/main/java/com/commonsware/android/rvp/PageAdapter.java)

Also note that when we set up the buffers in the constructor, we call a getNextTitle() method. This tracks the number of titles that we have generated, via a nextTabNumber field, and creates a new title with a new number on each call. This further decouples our tab titles from any concept of tab positions.

Adding Action Bar Items

We have four vector assets in res/drawable/ for our four action bar items:

<menu xmlns:android="http://schemas.android.com/apk/res/android">
  <item
    android:id="@+id/add"
    android:icon="@drawable/ic_add_white_24dp"
    android:showAsAction="ifRoom"
    android:title="@string/menu_add" />
  <item
    android:id="@+id/split"
    android:icon="@drawable/ic_split_white_24dp"
    android:showAsAction="ifRoom"
    android:title="@string/menu_split" />
  <item
    android:id="@+id/swap"
    android:icon="@drawable/ic_swap_white_24dp"
    android:showAsAction="ifRoom"
    android:title="@string/menu_swap" />
  <item
    android:id="@+id/remove"
    android:icon="@drawable/ic_remove_white_24dp"
    android:showAsAction="ifRoom"
    android:title="@string/menu_remove" />
</menu>
(from RecyclerViewPager/DynSnap/app/src/main/res/menu/actions.xml)

Those items get inflated and applied in MainActivity:

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.actions, menu);
    remove=menu.findItem(R.id.remove);

    return(super.onCreateOptionsMenu(menu));
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch(item.getItemId()) {
      case R.id.add:
        add();
        return(true);

      case R.id.split:
        split();
        return(true);

      case R.id.swap:
        swap();
        return(true);

      case R.id.remove:
        remove();
        return(true);
    }

    return(super.onOptionsItemSelected(item));
  }

(from RecyclerViewPager/DynSnap/app/src/main/java/com/commonsware/android/rvp/MainActivity.java)

Each of the items gets tied to its own dedicated method (e.g., R.id.add triggers a call to add()).

Note that we find and hold onto the remove MenuItem, caching its value in a field. We are going to need to disable this MenuItem when we can no longer allow the user to remove pages. In the case of this app, we prevent the user from removing all the pages, so once we are down to our last page, we need to disable the MenuItem, re-enabling it when the user adds new pages.

Adding Pages

The “add” and “split” operations both do the same thing: add a page. One difference is where they add it. “Add” adds a page before the current one. “Split” adds a page after the current one.

The add() and split() methods therefore do very similar things:

  private void add() {
    int current=getCurrentPosition();

    adapter.insert(current);
    tabs.addTab(tabs.newTab().setText(adapter.getTabText(current)), current);
    updateRemoveMenuItem();
  }

  private void split() {
    int newPosition=getCurrentPosition()+1;

    adapter.clone(newPosition-1);
    tabs.addTab(tabs.newTab().setText(adapter.getTabText(newPosition)), newPosition);
    updateRemoveMenuItem();
  }

(from RecyclerViewPager/DynSnap/app/src/main/java/com/commonsware/android/rvp/MainActivity.java)

Here, updateRemoveMenuItem() simply sets the enabled state of the remove item based on our page count:

  private void updateRemoveMenuItem() {
    remove.setEnabled(adapter.getItemCount()>1);
  }

(from RecyclerViewPager/DynSnap/app/src/main/java/com/commonsware/android/rvp/MainActivity.java)

Another difference between “add” and “split” is what the new page has for content. “Add” adds an empty page. “Split” clones the prose from the selected page and puts that prose in the new page as well.

That is why add() on MainActivity calls an insert() method on PageAdapter and split() calls clone(), because the PageAdapter has different behavior for each:

  void insert(int position) {
    buffers.add(position, new EditBuffer(getNextTitle()));
    notifyItemInserted(position);
  }

  void clone(int position) {
    updateProse(position);

    EditBuffer newBuffer=new EditBuffer(getNextTitle(),
      buffers.get(position).getProse());

    buffers.add(position+1, newBuffer);
    notifyItemInserted(position+1);
  }

(from RecyclerViewPager/DynSnap/app/src/main/java/com/commonsware/android/rvp/PageAdapter.java)

In both cases, a new EditBuffer gets added to the buffers, and we call notifyItemInserted() to let the RecyclerView know of the data model change. In the case of clone(), we also update the EditBuffer for the page to be cloned, then use that data to populate the new EditBuffer.

Moving Pages

The swap() method on MainActivity flips the positions of two pages. Normally, this will be the active tab and the next tab, but if the active tab is the last tab, the pair will be the active tab and the previous tab.

Hence, swap() identifies the right pair of positions, tells the PageAdapter to swap the contents, then swaps the titles of the two affected tabs:

  private void swap() {
    int first=getCurrentPosition();
    int second;

    if (first>=adapter.getItemCount()-1) {
      second=first;
      first--;
    }
    else {
      second=first+1;
    }

    adapter.swap(first, second);

    TabLayout.Tab firstTab=tabs.getTabAt(first);
    TabLayout.Tab secondTab=tabs.getTabAt(second);
    CharSequence firstText=firstTab.getText();

    firstTab.setText(secondTab.getText());
    secondTab.setText(firstText);
  }

(from RecyclerViewPager/DynSnap/app/src/main/java/com/commonsware/android/rvp/MainActivity.java)

The swap() method on PageAdapter needs to do three things:

  void swap(int first, int second) {
    EditBuffer firstBuffer=buffers.get(first);
    EditBuffer secondBuffer=buffers.get(second);

    buffers.set(first, secondBuffer);
    buffers.set(second, firstBuffer);

    PageController holder=
      (PageController)pager.findViewHolderForAdapterPosition(first);

    if (holder!=null) {
      holder.setText(secondBuffer.getProse());
    }

    holder=(PageController)pager.findViewHolderForAdapterPosition(second);

    if (holder!=null) {
      holder.setText(firstBuffer.getProse());
    }

    notifyItemChanged(first);
    notifyItemChanged(second);
  }

(from RecyclerViewPager/DynSnap/app/src/main/java/com/commonsware/android/rvp/PageAdapter.java)

Removing Pages

Removing a page involves:

The latter two tasks are handled by remove() on MainActivity:

  private void remove() {
    final int current=getCurrentPosition();

    tabs.removeTabAt(current);
    adapter.remove(current);

    if (current<adapter.getItemCount()) {
      pager.scrollToPosition(current);
    }

    updateRemoveMenuItem();
  }

(from RecyclerViewPager/DynSnap/app/src/main/java/com/commonsware/android/rvp/MainActivity.java)

The former two tasks are handled by remove() on PageAdapter:

  void remove(int position) {
    buffers.remove(position);
    notifyItemRemoved(position);
  }

(from RecyclerViewPager/DynSnap/app/src/main/java/com/commonsware/android/rvp/PageAdapter.java)