ViewPager2 in Android: Getting Started

In this tutorial for Android, you’ll learn to use the new ViewPager2 widget. Along the way you will build an app to show celebrity doppelganger animals. By Rajdeep Singh.

Leave a rating/review
Download materials
Save for later
Share

If you’ve used Android apps for some time, you’ve likely noticed many apps feature a common swipeable screen pattern. During the onboarding process, many apps use this pattern to showcase their major features one at a time. Another popular use case is using swipeable tabs to show different sections of the app.

Most developers use the ViewPager widget to develop these screens. On November 20, 2019, Google announced the stable version of ViewPager2, which replaces the old ViewPager and fixes many of its weaknesses.

In this tutorial, you’ll build Animal Doppleganger, an app that shows celebrity doppelganger animals, and learn:

  • How ViewPager2 works and how it differs from the previous ViewPager.
  • How to use ViewPager2 with Fragments.
  • What goes on under the hood of FragmentStateAdapter, a replacement for FragmentStatePagerAdapter.
  • How to implement vertical swiping with ViewPager2.
Note: This tutorial assumes you have previous experience developing for Android in Kotlin. If you’re unfamiliar with Kotlin, take a look at this tutorial. If you’re also new to Android development, check out the Getting Started with Android tutorials.

Getting Started

Click the Download Materials button at the top or bottom of the page to download the starter and final projects.

Now, open Android Studio and click Open an existing Android Studio project.

Android Studio start new project wizard

Navigate to the starter project directory you downloaded and click Open.

Starter project selected new project wizard

Take some time to familiarize yourself with the code.

Project Structure

You’ll find the following files in the starter project:

  • MainActivity.kt is the main class for this tutorial. You’ll add ViewPager2 related code primarily in this class.
  • DoppelgangerFragment.kt is a class you’ll use to add doppelganger images in your app. It extends Fragment and shows different images based on the value passed in its getInstance(position: Int) method.
  • A directory named doppelgangers inside the assets directory holds six photos named 0 to 5 with the .jpg extension.

Now that you have an overview of the files in this project, build and run the app. You’ll see a screen like this:

Animal doppelganger begin project files

Introducing ViewPager2

Google announced ViewPager2 around Google IO/19 as the successor to the existing ViewPager. As of November 2019, a stable version of ViewPager2 is finally available. To start using this new component, you have to migrate to AndroidX, as it’s not available in the support library.

According to the official Google documentation, ViewPager2 is addressing most of its predecessor’s shortcomings, including right-to-left layout support, vertical orientation and modifiable Fragment collections. You’ll explore many of these features while working on the sample project.

What’s New Since ViewPager

Your app’s users may not notice much change apart from the smoother performance, but, as an app developer, you should notice many improvements.

ViewPager2 uses RecyclerView, one of the most used widgets in the Android world, to show collections of items. This is a huge change for the ViewPager API, as RecyclerView is a powerful and robust widget.

Using all of RecyclerView’s benefits, ViewPager 2 provides these improvements:

  • It supports vertical paging by using LinearLayoutManager. This means you can easily switch between horizontal and vertical orientations.
  • It provides support for right-to-left, or RTL, layouts.
  • It supports the use of a DiffUtil by default, since it’s a RecyclerView. This lets you dynamically add Fragments or Views and then notify the ViewPager2 to update the UI.

Enough with the theory, time to start coding!

Implementing a Basic Swipeable Screen

In this section, you’ll modify the starter project to show swipeable sections of animal doppelgangers instead of showing only Doguel Jackson.

This requires the following:

  1. Adding the dependency for androidx.viewpager2:viewpager2 in the app level build.gradle.
  2. Adding the ViewPager2 XML widget in the layout file.
  3. Wiring the ViewPager2 with custom FragmentStateAdapter.

Adding the ViewPager2 Widget

In the starter project, select the Android view of the project files if not already selected.

Android project mode selection opened in Android Studio

Now, open build.gradle (Module: app) and add the following below the line marking TODO:1:

implementation 'androidx.viewpager2:viewpager2:1.0.0'

The code above tells Gradle that your app module depends on androidx.viewpager2:viewpager2. With the line added, sync Gradle by clicking the Sync Now button that shows up at the top of the file. After a successful sync, you can start using ViewPager2 in the project!

Right now, the app shows a single Fragment in MainActivity. It showcases my all-time favorite Avenger, Doguel Jackson.

Before you can display other famous doppelgangers, you need to remove some existing code. Open MainActivity.kt and remove the following code:

supportFragmentManager.beginTransaction()
        .add(R.id.fragmentContainer, DoppelgangerFragment.getInstance(0)).commit()

Now, open activity_main.xml, which is the layout file of MainActivity, and remove this code snippet:

<FrameLayout
  android:id="@+id/fragmentContainer"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />

This Framelayout contained a single instance of the Fragment, but you’ll want to show multiple ones instead!

Build and run the app to make sure you see a blank screen like this:

Android emulator showing empty app screen

If your app screen looks as blank as one’s mind tends to be during an important exam, you can start adding the ViewPager2 related code. With activity_main.xml still open, add the following lines below TODO:2:

<androidx.viewpager2.widget.ViewPager2
  android:id="@+id/doppelgangerViewPager"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />

This adds a ViewPager2 widget in your layout with the ID doppelgangerViewPager. You’ll use it to refer to this view in your Kotlin code. This view covers the entire screen because of the match_parent value for both layout_width and layout_height.

You won’t see any change if you try to run the app at this point because ViewPager2 on its own is simply a ViewGroup. The magic happens when you add a data adapter to it, which you’ll do in the next section.

Wiring Your ViewPager2 With a FragmentStateAdapter

In this section, you’ll create a custom FragmentStateAdapter and wire it with your ViewPager2 widget. You’ll learn more about how exactly FragmentStateAdapter works later.

Create a new Kotlin file and name it DoppelgangerAdapter. Paste the following code inside:

import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter

class DoppelgangerAdapter(activity: AppCompatActivity, val itemsCount: Int) :
    FragmentStateAdapter(activity) {

  override fun getItemCount(): Int {
    return itemsCount
  }

  override fun createFragment(position: Int): Fragment {
    return DoppelgangerFragment.getInstance(position)
  }
}

Here’s a breakdown of the code:

  • You declare a class named DoppelgangerAdapter which takes two parameters. The first is a reference to the Activity which hosts the adapter, and the second is an Int that tells the adapter the number of items it’ll show. The Activity reference is passed to the super constructor. You’ll see why this is required later.
  • Next, you override getItemCount() which is an abstract method that returns the total number of items in the adapter.
  • Finally, you override createFragment(position: Int), which is again an abstract method and returns a Fragment instance for the given position. DoppelgangerFragment.getInstance(position) creates instances of DoppelgangerFragment with a different doppelganger picture for different values of position.

Here comes the last step before your ViewPager2 starts working. Go to MainActivity.kt and add the following import statement:

import kotlinx.android.synthetic.main.activity_main.*

Then, add the following lines below TODO:3:

val doppelgangerAdapter = DoppelgangerAdapter(this, doppelgangerNamesArray.size)
doppelgangerViewPager.adapter = doppelgangerAdapter

In the code above, you created an instance of your DoppelgangerAdapter, passing in the current instance of MainActivity and the size of doppelgangerNamesArray, which is the number of pages you want to show.

MainActivity already had doppelgangerNamesArray defined, which is an array of strings containing the names of celebrities. It gets the value from strings.xml in res/values.

Give yourself a pat on the back!

Build and run the app and you’ll see Doguel Jackson again. But if you swipe, you’ll see other doppelgangers, too. The app should behave like this:

Animal doppelganger ViewPager screen being swiped right to left

Under the Hood of FragmentStateAdapter

Note: This optional theory section will help you understand what goes on inside FragmentStateAdapter. If you’d like to continue the implementation part of this tutorial, feel free to skip ahead to the Listening for Page Changes section.

Did you decide to give it a read? Good! This will be quick and worth your time.

In the last section, you created DoppelgangerAdapter which extended FragmentStateAdapter. In this section, you’ll see what goes on inside FragmentStateAdapter by diving into specific parts of its code.

Open DoppelgangerAdapter and navigate to the declaration of FragmentStateAdapter by clicking ⌘ + Click on macOS or Ctrl + Click on Windows. You’ll see the class declaration like this:

public abstract class FragmentStateAdapter extends 
  RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter {
}

After looking at the code above, you know that ViewPager2 uses RecyclerView.Adapter with some changes to show views. This means you can extend your adapter directly from this base class in case you want to show instances of View class like you do for any other RecyclerView.

So why extend FragmentStateAdapter at all? It comes with a lot of code to handle Fragment-related stuff for you. Remember the Activity instance you passed in to its constructor? This is how that instance is used:

public FragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) {
  this(fragmentActivity.getSupportFragmentManager(), 
    fragmentActivity.getLifecycle());
}

The adapter stores the FragmentManager and Lifecycle of the Activity and uses them to handle adding Fragments in the ViewHolder and managing them during lifecycle events.

If you’ve used a RecyclerView Adapter before, then you know the two most important methods you have to override: onCreateViewHolder and onBindViewHolder.

Overriding onCreateViewHolder

Take a look at onCreateViewHolder in FragmentStateAdapter:

@NonNull
@Override
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
  return FragmentViewHolder.create(parent);
}

It calls create() on FragmentViewHolder which is simply a ViewHolder. Take a look at create() in FragmentViewHolder:

@NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
  FrameLayout container = new FrameLayout(parent.getContext());
  container.setLayoutParams(
    new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
      ViewGroup.LayoutParams.MATCH_PARENT));
  container.setId(ViewCompat.generateViewId());
  container.setSaveEnabled(false);
  return new FragmentViewHolder(container);
}

It returns a FragmentViewHolder instance with a FrameLayout added as the itemView.

Overriding onBindViewHolder

The other main part of Adapter is onBindViewHolder. Below is a extremely simplified version of it:

@Override
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
  ...
  ensureFragment(position);
  ...
  gcFragments();
}

This methods performs some sanity checks and then calls ensureFragment(position). This method checks if the Fragment exists and, if it doesn’t, it calls the createFragment(position) you’ve over-riden in your custom adapter.

It then has some code to adapt to layout changes. Next comes another interesting part, where it calls gcFragments(). It removes the Fragments which are no longer part of the data set or fall in the category of stale Fragments, for which it has checks. This ensures that ViewPager2 is memory efficient with Fragments.

This is a brief look at this adapter, as its many features are beyond the scope of this article. Feel free to explore them on your own and ask questions in the forum if you have any doubts.

Listening for Page Changes

While using ViewPager2, you may want to know when the screen state changes from scrolling to idle or when the user selects a new page on the screen.

If you’ve used ViewPager, you used the OnPageChangeListener interface. With OnPageChangeListener, you have to override all the methods in the interface, whether you want to use them or not.

ViewPager2 provides a similar API to listen to page changes. But, instead of using an interface, it uses an abstract class, OnPageChangeCallback, which provides an empty implementation for its three methods. This way, you only need to override the methods you need!

Add the following import statements in MainActivity:

import android.widget.Toast
import androidx.viewpager2.widget.ViewPager2

Next, add the following code below TODO:4 in MainActivity:

var doppelgangerPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
  override fun onPageSelected(position: Int) {
    Toast.makeText(this@MainActivity, "Selected position: ${position}", 
      Toast.LENGTH_SHORT).show()
  }
}

This code uses Kotlin’s object expression to anonymously implement ViewPager2.OnPageChangeCallback and overrides only onPageSelected(position: Int). It then shows a Toast every time a new page is selected.

OnPageChangeCallback has three methods:

  • onPageScrolled(int position, float positionOffset, @Px int positionOffsetPixels): ViewPager2 invokes this method when the currently selected page starts scrolling.
  • onPageSelected(int position): When a new page is selected, ViewPager 2 invokes this method.
  • onPageScrollStateChanged(@ScrollState int state): ViewPager 2 invokes this when the scroll state of the current page changes.

Now you need to wire your doppelgangerViewPager with the doppelgangerPageChangeCallback so it can start to get the callbacks. In MainActivity, find TODO:5 and add the following:

doppelgangerViewPager.registerOnPageChangeCallback(doppelgangerPageChangeCallback)

You need to unregister the callback when you no longer want to listen to the changes. In onDestroy(), add the following code below TODO:6:

doppelgangerViewPager.unregisterOnPageChangeCallback(doppelgangerPageChangeCallback)

Build and run the app. You’ll see a Toast message whenever you swipe the page.

Project app being swiped with Toast message shown for each swipe

Trying the Vertical Swipe

ViewPager2 comes with an option to switch orientation easily, which was tough to do using the original ViewPager. This is now easy because ViewPager 2 uses a RecyclerView which in turn uses a LayoutManager to manage the positioning of views inside it.

Paste the following code under TODO:7 in MainActivity.kt:

doppelgangerViewPager.orientation = ViewPager2.ORIENTATION_VERTICAL

This orientation property will change which way the pages swipe. It’s that easy!

Build and run the app. Try to swipe vertically this time.

Project screen being vertically swiped in emulator

Look at the implementation of the orientation keyword by selecting ⌘ + Click on macOS or Ctrl + Click on Windows. Android Studio takes you to the ViewPager2 class where you see the following:

/**
* Sets the orientation of the ViewPager2.
*
* @param orientation {@link #ORIENTATION_HORIZONTAL} or {@link #ORIENTATION_VERTICAL}
*/
public void setOrientation(@Orientation int orientation) {
  mLayoutManager.setOrientation(orientation);
  mAccessibilityProvider.onSetOrientation();
}

Now you know for sure this method does nothing but call setOrientation on the underlying LinearLayoutManager.

Thank you, RecyclerView! :]

Connecting a TabLayout With ViewPager2

Showing tabs whenever you use swipeable screens is a good way to let the users know there’s more to see by swiping. The process of adding tabs has changed a bit with ViewPager2.

Android provides TabLayout, a widget to show tabs. Earlier it came with the support library but since the migration to AndroidX, it’s part of the com.google.android.material package.

Open the app level build.gradle again and paste the following code under TODO:8:

implementation 'com.google.android.material:material:1.2.0-alpha04'

Sync the project with Gradle files. With that in place, you can now use TabLayout in your project.

Open activity_main.xml and paste the following code under TODO:9:

<com.google.android.material.tabs.TabLayout
  android:id="@+id/tabLayout"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:background="@color/colorPrimary"
  app:tabMode="scrollable"
  app:tabTextColor="@android:color/white" />

Your xml shows a namespace error for app, which you can fix by adding this line in the parent LinearLayout. A quick fix should also show up for this. To apply it, select ⌥ + Enter on macOS or Alt + Enter on Windows.

xmlns:app="http://schemas.android.com/apk/res-auto"

Here’s a breakdown of the TabLayout widget code you added:

  • The android:id="@+id/tabLayout" attribute sets the ID of the TabLayout. You’ll use this later in Kotlin to refer to this view.
  • layout_width is set to match_parent and layout_height is set to wrap_content. So the tabs cover the full width of the screen and take only as much height as they need.
  • android:background="@color/colorPrimary" sets the background color of the tabs to match the primary color of the app.
  • app:tabMode="scrollable" makes the tabs scrollable in case the number of items in the TabLayout requires more space than available on the screen. Other possible values are auto and fixed. Try these on your own later!
  • app:tabTextColor="@android:color/white" sets the text color on the tabs to white.

Using TabsLayoutMediator

Now, here comes the tricky part. To link the TabLayout and ViewPager2, you have to use TabLayoutMediator. This synchronizes the ViewPager and TabLayout to change the position when one gets clicked or swiped.

Take a look at its documentation to learn more about it.

Open MainActivity.kt and comment out the line that sets the orientation to vertical, as it’ll look better to use horizontal swiping with tabs. Add the following import statement in MainActivity:

import com.google.android.material.tabs.TabLayoutMediator

Next, add the following code under TODO:10:

TabLayoutMediator(tabLayout, doppelgangerViewPager) { tab, position ->
  //To get the first name of doppelganger celebrities
  tab.text = doppelgangerNamesArray[position].split(" ")[0]
}.attach()

Now, if you look at the constructor of TabLayoutMediator, you’ll understand what’s happening in the above code. This is how it looks:

public TabLayoutMediator(
  @NonNull TabLayout tabLayout,
  @NonNull ViewPager2 viewPager,
  @NonNull TabConfigurationStrategy tabConfigurationStrategy) {
  this(tabLayout, viewPager, true, tabConfigurationStrategy);
}

It expects three parameters. These are a TabLayout, a ViewPager2 and an interface called TabConfigurationStrategy, which looks like this:

/**
* A callback interface that must be implemented to set the text and styling of newly created
* tabs.
*/
public interface TabConfigurationStrategy {
  /**
   * Called to configure the tab for the page at the specified position. Typically calls {@link
   * TabLayout.Tab#setText(CharSequence)}, but any form of styling can be applied.
   *
   * @param tab The Tab which should be configured to represent the title of the item at the given
   *     position in the data set.
   * @param position The position of the item within the adapter's data set.
   */
  void onConfigureTab(@NonNull TabLayout.Tab tab, int position);
}

Thanks to the beloved Kotlin syntax, if the last parameter to a function is a function, or, in this case, a functional interface, and you’re passing a lambda expression, you can specify it outside the parentheses.

onConfigureTab is called for each item in the adapter when they are populated. The above code sets the text for each of the tabs with the first name of each doppelganger.

The important thing to notice is attach(). This method links the functionalities of ViewPager2 and TabLayout together.

Build and run the app. You’ll see the tabs working now.

Project app with tab layout being swiped

Trying the RTL Support

RTL stands for right-to-left. In languages like Arabic, where words go from right to left, it’s essential you take measures to ensure your app doesn’t break. Supporting RTL in ViewPager isn’t easy.

However, ViewPager2 comes with RTL support. To try it, you can either force your entire device to RTL mode from Settings or write some Kotlin code to force the tabs and ViewPager2. You’ll try the latter approach.

Add the following import statement in MainActivity:

import android.view.View

Copy the following code below TODO:11:

doppelgangerViewPager.layoutDirection = ViewPager2.LAYOUT_DIRECTION_RTL
tabLayout.layoutDirection = View.LAYOUT_DIRECTION_RTL

The code above forces the layout direction of both widgets to RTL. Build and run the app to see the change.

Android app screen being swiped in right to left support format

Where to Go From Here?

You can download the final project using the Download Materials button at the top or bottom of this tutorial.

In this tutorial, you learned to use ViewPager2 with Fragments to display animal doppelgangers, connected a TabLayout to it and saw how easy it is to add vertical scrolling and RTL support.

If you want to learn more about ViewPager2, check out this official guide, which shows you how to create custom animations between your pages. You can also take a look at the documentation of the class itself.

If you have any questions or comments, please join the discussion below!