-
Notifications
You must be signed in to change notification settings - Fork 6.2k
ViewPager with FragmentPagerAdapter
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.ViewPagerand itsFragmentPagerAdapter/FragmentStatePagerAdaptercompanions — which earlier versions of this guide covered — are deprecated. Google's ViewPager2 migration guide walks through converting existing code; this page now coversViewPager2throughout.
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"
}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".
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
}
}
}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 byTabLayoutMediatorwhen 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.
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.
We can access the selected page within the ViewPager2 at any time with the currentItem property, which returns the current page:
vpPager.currentItem // --> 2The current page can also be changed programmatically:
vpPager.currentItem = 2
// or, to control whether the change animates:
vpPager.setCurrentItem(2, true) // second argument is smoothScrollWith this getter and setter, we can easily access or modify the selected page at runtime.
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).
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.
In this way, we can use the same pager system described above and augment the pager with a tabbed navigation indicator.
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.
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.
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.
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 = 3This can be useful in order to preload more fragments for a smoother viewing experience, trading off with memory usage.
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)))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.
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 = falsePage changes via setCurrentItem(...) (and fake drags via beginFakeDrag()) continue to work while user swipes are ignored.
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.
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.
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.
- https://developer.android.com/develop/ui/views/animations/screen-slide-2
- https://developer.android.com/develop/ui/views/animations/vp2-migration
- https://developer.android.com/reference/androidx/viewpager2/widget/ViewPager2
- https://developer.android.com/reference/androidx/viewpager2/adapter/FragmentStateAdapter
- https://developer.android.com/reference/com/google/android/material/tabs/TabLayoutMediator
- https://developer.android.com/jetpack/androidx/releases/viewpager2
Created by CodePath with much help from the community. Contributed content licensed under cc-wiki with attribution required. You are free to remix and reuse, as long as you attribute and use a similar license.
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.

