Skip to content

ViewPager with FragmentPagerAdapter

Mitchell Bryson edited this page Aug 5, 2026 · 70 revisions

Overview

A pager is a layout that allows the user to swipe left and right through "pages" of content which are usually different fragments. This is a common navigation mode to use alongside tabbed interfaces such as Google Play Style Tabs using TabLayout.

The modern pager widget is ViewPager2 (androidx.viewpager2.widget.ViewPager2), which is built on top of RecyclerView and pairs with the FragmentStateAdapter to display fragments as pages.

Note: The original androidx.viewpager.widget.ViewPager and its FragmentPagerAdapter / FragmentStatePagerAdapter companions — which earlier versions of this guide covered — are deprecated. Google's ViewPager2 migration guide walks through converting existing code; this page now covers ViewPager2 throughout.

ViewPager

Setup

Add the ViewPager2 dependency to your module-level build.gradle. The current stable release is 1.1.0 (May 2024, per the ViewPager2 release notes):

dependencies {
    implementation "androidx.viewpager2:viewpager2:1.1.0"
    // Needed later for TabLayout + TabLayoutMediator
    implementation "com.google.android.material:material:1.14.0"
}

Usage

Layout ViewPager2

A ViewPager2 is a layout which can be added to any layout XML file inside a root layout:

<?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"
    android:orientation="vertical">

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

Unlike the old ViewPager, there is no nested PagerTabStrip indicator view — a page indicator is now a sibling TabLayout wired up with TabLayoutMediator, covered in Tabbed Interface with Pager below. ViewPager2 also supports vertical paging out of the box via android:orientation="vertical".

Define Fragments

Next, let's suppose we have defined two fragments FirstFragment and SecondFragment, both of which contain a label in the layout and have implementations such as:

class FirstFragment : Fragment() {
    // Store instance variables
    private var title: String? = null
    private var page: Int = 0

    // Store instance variables based on arguments passed
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        page = requireArguments().getInt("someInt", 0)
        title = requireArguments().getString("someTitle")
    }

    // Inflate the view for the fragment based on layout XML
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_first, container, false)
        val tvLabel = view.findViewById<TextView>(R.id.tvLabel)
        tvLabel.text = "$page -- $title"
        return view
    }

    companion object {
        // newInstance constructor for creating fragment with arguments
        fun newInstance(page: Int, title: String): FirstFragment {
            val fragmentFirst = FirstFragment()
            val args = Bundle()
            args.putInt("someInt", page)
            args.putString("someTitle", title)
            fragmentFirst.arguments = args
            return fragmentFirst
        }
    }
}

Setup FragmentStateAdapter

Now we need to define the adapter that will properly determine how many pages exist and which fragment to display for each page by creating a FragmentStateAdapter. It replaces both of the old adapters (FragmentPagerAdapter and FragmentStatePagerAdapter) and only requires two overrides — getItemCount() and createFragment(position):

class MainActivity : AppCompatActivity() {
    // ...

    class MyPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {

        companion object {
            private const val NUM_ITEMS = 3
        }

        // Returns total number of pages
        override fun getItemCount(): Int = NUM_ITEMS

        // Returns the fragment to display for that page
        override fun createFragment(position: Int): Fragment {
            return when (position) {
                // Fragment # 0 - This will show FirstFragment
                0 -> FirstFragment.newInstance(0, "Page # 1")
                // Fragment # 1 - This will show FirstFragment with a different title
                1 -> FirstFragment.newInstance(1, "Page # 2")
                // Fragment # 2 - This will show SecondFragment
                else -> SecondFragment.newInstance(2, "Page # 3")
            }
        }
    }
}

Two behavioral differences from the old getItem(...) are worth noting, per the migration guide:

  • createFragment(...) must always return a new fragment instance — the adapter caches and restores fragments itself, so never hand back a stored instance.
  • There is no getPageTitle(...) override — page titles are supplied by TabLayoutMediator when using tabs (see below).

If the pager lives inside a fragment rather than an activity, pass the fragment to the constructor instead (FragmentStateAdapter(fragment)); the adapter will then use the fragment's childFragmentManager.

Apply the Adapter

Finally, let's associate the ViewPager2 with a new instance of our adapter:

class MainActivity : AppCompatActivity() {
    private lateinit var vpPager: ViewPager2

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_home)
        vpPager = findViewById(R.id.vpPager)
        vpPager.adapter = MyPagerAdapter(this)
    }

    // ...
}

And now we have a basic functioning pager with any number of fragments as pages which can be swiped between.

Selecting or Getting the Page

We can access the selected page within the ViewPager2 at any time with the currentItem property, which returns the current page:

vpPager.currentItem // --> 2

The current page can also be changed programmatically:

vpPager.currentItem = 2
// or, to control whether the change animates:
vpPager.setCurrentItem(2, true) // second argument is smoothScroll

With this getter and setter, we can easily access or modify the selected page at runtime.

Setup OnPageChangeCallback

If the Activity needs to be able to listen for changes to the page selected or other events surrounding the ViewPager2, then we just need to register a ViewPager2.OnPageChangeCallback to handle the events. Unlike the old OnPageChangeListener interface, the callback is a class with empty default implementations, so we only override the methods we need:

// Attach the page change callback inside the activity
vpPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {

    // This method will be invoked when a new page becomes selected.
    override fun onPageSelected(position: Int) {
        Toast.makeText(this@MainActivity,
            "Selected page position: $position", Toast.LENGTH_SHORT).show()
    }

    // This method will be invoked when the current page is scrolled
    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        // Code goes here
    }

    // Called when the scroll state changes:
    // SCROLL_STATE_IDLE, SCROLL_STATE_DRAGGING, SCROLL_STATE_SETTLING
    override fun onPageScrollStateChanged(state: Int) {
        // Code goes here
    }
})

Unregister the callback with unregisterOnPageChangeCallback(...) when it is no longer needed (for example in onDestroy).

Tabbed Interface with Pager

We can pair the ViewPager2 with a TabLayout in order to create tabs to display our fragments. The TabLayout sits as a sibling of the pager in the layout (not nested inside it):

<com.google.android.material.tabs.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:tabMode="fixed"
    app:tabGravity="fill" />

<androidx.viewpager2.widget.ViewPager2
    android:id="@+id/vpPager"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1" />

The two are connected with a TabLayoutMediator, which also supplies the tab titles (replacing the old adapter-level getPageTitle(...)):

val tabLayout = findViewById<TabLayout>(R.id.tabLayout)
TabLayoutMediator(tabLayout, vpPager) { tab, position ->
    tab.text = "Page ${position + 1}"
}.attach()

See Google Play Style Tabs using TabLayout for a full walkthrough of styling the tabbed interface.

Tabs

In this way, we can use the same pager system described above and augment the pager with a tabbed navigation indicator.

Dynamic ViewPager Fragments

In certain cases, we may require a dynamic pager where pages are added or removed at runtime, or where we want access to the fragment instances the pager created. With the old ViewPager this required the alternate FragmentStatePagerAdapter plus a custom "smart" caching subclass; FragmentStateAdapter already handles state saving and fragment caching internally, so no extra base class is needed.

Access Fragment Instances

FragmentStateAdapter adds each page fragment to the FragmentManager under the tag "f" + itemId, and the default getItemId(position) implementation returns the position (see the FragmentStateAdapter source). So as long as you have not overridden getItemId(...), an existing page fragment can be looked up by position:

// Gets the fragment for the first page, if it has been created
val firstFragment = supportFragmentManager.findFragmentByTag("f0") as? FirstFragment

// Gets the fragment for the currently displayed page
val currentFragment = supportFragmentManager.findFragmentByTag("f${vpPager.currentItem}")

Use childFragmentManager instead if the adapter was constructed with a Fragment. Note the lookup returns null for pages the pager has not created yet, so prefer communicating with page fragments through a shared ViewModel where possible rather than reaching into the pager.

Replacing Fragments Inside ViewPager2

In certain cases, we want to dynamically replace the Fragment shown for a given page. For example, perhaps the page currently displays a list of items and we want a detail view to show up when an item is selected. Because FragmentStateAdapter is a RecyclerView.Adapter, this is modeled as a data change: keep the page state in a backing collection, give each page a stable id, and notify the adapter when the state changes. The old getItemPosition(...) / POSITION_NONE mechanism is gone; its replacement is the getItemId(...) / containsItem(...) pair:

class MyPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
    // One stable id per page; change an entry to replace that page's fragment
    private val pageIds = mutableListOf(1L, 2L, 3L)

    override fun getItemCount(): Int = pageIds.size

    override fun createFragment(position: Int): Fragment {
        // Return the fragment matching the page id/state at this position
        return when (pageIds[position]) {
            // ...
            else -> FirstFragment.newInstance(position, "Page # ${position + 1}")
        }
    }

    // Provide stable, unique ids so the adapter can detect page changes
    override fun getItemId(position: Int): Long = pageIds[position]

    override fun containsItem(itemId: Long): Boolean = pageIds.contains(itemId)

    // Replace the page at the given position with a new fragment
    fun replacePage(position: Int, newPageId: Long) {
        pageIds[position] = newPageId
        notifyItemChanged(position)
    }
}

When a page's id changes, the adapter destroys the old fragment and calls createFragment(...) again for that position. An alternative approach that avoids adapter changes entirely is to have the page display a fragment container that switches between multiple child content fragments itself.

Set Offscreen Page Limit

By default (OFFSCREEN_PAGE_LIMIT_DEFAULT), ViewPager2 lazily creates pages and lets the underlying RecyclerView recycle them as the user scrolls. You can instead use offscreenPageLimit to force a fixed number of page instances to be kept attached on either side of the current page. As a result, more memory will be consumed, so be careful when tweaking this setting if your pages have complex layouts.

For example, to keep 3 page instances on both sides of the current page:

vpPager.offscreenPageLimit = 3

This can be useful in order to preload more fragments for a smoother viewing experience, trading off with memory usage.

ViewPager2 with Visible Adjacent Pages

If you are interested in a pager where the adjacent pages are partially visible ("page peeking"), the old getPageWidth(...) override no longer exists — per the migration guide, the effect is achieved with padding and clipToPadding on the pager's internal RecyclerView, usually combined with a MarginPageTransformer:

vpPager.offscreenPageLimit = 1
// The first (and only) child of ViewPager2 is its internal RecyclerView
(vpPager.getChildAt(0) as RecyclerView).apply {
    val padding = resources.getDimensionPixelOffset(R.dimen.pager_peek_width)
    setPadding(padding, 0, padding, 0)
    clipToPadding = false
}
// Adds spacing between the pages
vpPager.setPageTransformer(MarginPageTransformer(
    resources.getDimensionPixelOffset(R.dimen.pager_page_margin)))

Animating the Scroll with PageTransformer

We can customize how the pages animate as they are being swiped between using a ViewPager2.PageTransformer. Unlike the old two-argument setPageTransformer(reverseDrawingOrder, transformer), the ViewPager2 version takes just the transformer, and since PageTransformer is a single-method interface it can be supplied as a lambda:

vpPager.setPageTransformer { page, position ->
    // Do our transformations to the pages here
}

The transformPage lambda receives two parameters: page, which is the particular page view to be modified, and position, which indicates where that page is located relative to the center of the screen. The page which fills the screen is at position 0. The page immediately to the right is at position 1. If the user scrolls halfway between pages one and two, page one has a position of -0.5 and page two has a position of 0.5.

vpPager.setPageTransformer { page, position ->
    when {
        position < -1 -> { // [-Infinity,-1)
            // This page is way off-screen to the left.
            page.alpha = 0f
        }
        position <= 1 -> { // Page to the left, page centered, page to the right
            // Modify page view animations here for pages in view
        }
        else -> { // (1,+Infinity]
            // This page is way off-screen to the right.
            page.alpha = 0f
        }
    }
}

Multiple effects can be combined with a CompositePageTransformer (for example a MarginPageTransformer plus a custom scale effect). For a complete worked example including a "zoom out" and a "depth" transformer, see the official screen slide guide.

Disabling Swipe Events

With the old ViewPager, disabling swiping required a custom subclass that intercepted touch events. ViewPager2 has this built in — to have a pager whose page is only changed programmatically (for example by an indicator or buttons), simply disable user input:

vpPager.isUserInputEnabled = false

Page changes via setCurrentItem(...) (and fake drags via beginFakeDrag()) continue to work while user swipes are ignored.

Launching an Activity with Tab Selected

Often when launching a tabbed activity, there needs to be a way to select a particular tab to be displayed once the activity loads. For example, an activity has three tabs with one tab being a list of created posts. After a user creates a post on a separate activity, the user needs to be returned to the main activity with the "new posts" tab displayed. This can be done through the use of intent extras and the currentItem property. First, when launching the tabbed activity, we need to pass in the selected tab as an extra:

/* In creation activity that wants to launch a tabbed activity */
val intent = Intent(this, MyTabbedActivity::class.java)
// Pass in tab to be displayed
intent.putExtra(MyTabbedActivity.SELECTED_TAB_EXTRA_KEY, MyTabbedActivity.NEW_POSTS_TAB)
// Start the activity
startActivity(intent)

If the activity needs to return a result, we can also return this as an activity result. Next, we can read this information from the intent within the tabbed activity:

/* In tabbed activity */
class MyTabbedActivity : AppCompatActivity() {

    companion object {
        const val SELECTED_TAB_EXTRA_KEY = "selectedTabIndex"
        const val HOME_TAB = 0
        const val FAVORITES_TAB = 1
        const val NEW_POSTS_TAB = 2
    }

    private lateinit var vpPager: ViewPager2

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        vpPager = findViewById(R.id.vpPager)
        // ... adapter setup ...
        // Set the selected tab
        setSelectedTab()
    }

    // Reads selected tab from launching intent and sets page accordingly
    private fun setSelectedTab() {
        // Fetch the selected tab index with default
        val selectedTabIndex = intent.getIntExtra(SELECTED_TAB_EXTRA_KEY, HOME_TAB)
        // Switch to page based on index
        vpPager.currentItem = selectedTabIndex
    }
}

With that, any activity can launch the tabbed activity with the ability to configure the selected tab.

Custom Pages without Fragments

While a ViewPager2 is often coupled with a Fragment for each page using the FragmentStateAdapter, there are cases where the pages are better off as plain views.

A good example is an image gallery, where the user can swipe between different pictures. Since ViewPager2 is built on RecyclerView, this no longer requires a special PagerAdapter — we simply attach a regular RecyclerView.Adapter (see Using the RecyclerView):

// Custom pager adapter not using fragments
class CustomPagerAdapter(private val pages: List<Page>) :
    RecyclerView.Adapter<CustomPagerAdapter.PageViewHolder>() {

    class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val imageView: ImageView = itemView.findViewById(R.id.imageView)
    }

    // Returns the number of pages to be displayed in the ViewPager2.
    override fun getItemCount(): Int = pages.size

    // This method creates the page view for the given position.
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
        // Inflate the layout for the page
        val itemView = LayoutInflater.from(parent.context)
            .inflate(R.layout.pager_item, parent, false)
        return PageViewHolder(itemView)
    }

    // Populates data into the page (i.e. sets the image)
    override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
        val page = pages[position]
        // holder.imageView.setImageResource(...)
    }
}

One ViewPager2-specific requirement: the root view of pager_item.xml must use match_parent for both layout_width and layout_height — pages must fill the whole pager or ViewPager2 throws an IllegalStateException at runtime.

Custom ViewPager Indicators

An "indicator" is the UI element that displays the possible pages and the current page, such as tabs or a row of dots. For ViewPager2, the standard approach is the TabLayout + TabLayoutMediator combination shown above — dot-style indicators can be achieved by styling the tabs (a drawable app:tabBackground selector with app:tabIndicatorHeight="0dp"). The third-party indicator libraries this page previously listed (SpringIndicator, InkPageIndicator, CircleIndicator) targeted the deprecated ViewPager and have been unmaintained for years — the SpringIndicator repository no longer exists, and the others last saw commits in 2018 and 2021 respectively — so they are no longer recommended.

References

Finding these guides helpful?

We need help from the broader community to improve these guides, add new topics and keep the topics up-to-date. See our contribution guidelines here and our topic issues list for great ways to help out.

Check these same guides through our standalone viewer for a better browsing experience and an improved search. Follow us on twitter @codepath for access to more useful Android development resources.

Clone this wiki locally