ViewPager / ViewPager2

ViewPager2

ViewPager has been around for a number of years now, and was added in support library V22.1.0. In February 2019 the first alpha of ViewPager2 was released. In this post we’ll take a look at how ViewPager2 differs from ViewPager, and how to implement it.

Image by OpenIcons from Pixabay

The main difference between ViewPager2 and the original ViewPager is that ViewPager2 is implemented around RecyclerView which leverages much of the behaviour implicit in RecyclerView which already contains mechanisms to destroy and recycle items when the move outside the visible area of the RecyclerView and these same mechanisms are used to recycle the pages in ViewPager2. Moreover, the behaviour RecyclerView is highly extensible and configurable, and we are therefore able to customise the appearance and behaviour of ViewPager2 as a result.

I’m assuming that the reader is already familiar with the basics of RecyclerView as it is such an important and useful component in the Android UI Framework.

The first thing that we need to do is add the ViewPager2 dependency:

.
.
.
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
    implementation "androidx.appcompat:appcompat:1.1.0-beta01"
    implementation "androidx.core:core-ktx:1.2.0-alpha02"
    implementation "com.google.android.material:material:1.1.0-alpha07"
    implementation "androidx.constraintlayout:constraintlayout:2.0.0-beta2"
    implementation "androidx.lifecycle:lifecycle-extensions:2.0.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0"
    implementation "androidx.viewpager2:viewpager2:1.0.0-alpha05"
}
.
.
.

The next thing we’ll do is add a simple Fragment which takes an argument indicating which page it represents:

class PageFragment : Fragment() {

    companion object {

        private const val ARG_SECTION_NUMBER = "section_number"
        @JvmStatic
        fun newInstance(sectionNumber: Int): PageFragment {
            return PageFragment().apply {
                arguments = Bundle().apply {
                    putInt(ARG_SECTION_NUMBER, sectionNumber)
                }
            }
        }
    }

    private lateinit var viewModel: PageViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel = ViewModelProviders.of(this).get(PageViewModel::class.java).apply {
            setIndex(arguments?.getInt(ARG_SECTION_NUMBER) ?: 1)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val root = inflater.inflate(R.layout.page_fragment, container, false)
        val textView: TextView = root.findViewById(R.id.section_label)
        viewModel.text.observe(this, Observer {
            textView.text = it
        })
        return root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel = ViewModelProviders.of(this).get(PageViewModel::class.java).apply {
            setIndex(arguments?.getInt(ARG_SECTION_NUMBER) ?: 1)
        }
    }
}

This gets backed by a ViewModel that will provide the string to populate the TextView in the Fragment. This Fragment / ViewModel is very similar to those that get generated if you create a new Android project with the “Tabbed Activity” template from the “Create New Project” wizard in Android Studio. Hopefully it’s all pretty self-explanatory, so let’s move on and get the the are of interest: wiring this up to ViewPager2.

We discussed previously how ViewPager2 is built around RecyclerView. Where the original ViewPager used its own PagerAdapter and subclasses, ViewPager2 uses RecyclerView.Adapter instead, but provides its own subclasses to make things easier for us.

We’ll create a subclass of FragmentStateAdapter which will manage our Fragment state in the case of configuration changes (i.e. device rotation):

class SectionsPagerAdapter(
    private val activity: FragmentActivity
) : FragmentStateAdapter(activity) {

    override fun createFragment(position: Int): Fragment =
        PageFragment.newInstance(position)

    fun getPageTitle(position: Int): CharSequence =
        activity.resources.getString(TAB_TITLES[position])

    override fun getItemCount(): Int = TAB_TITLES.size

    companion object {
        private val TAB_TITLES = arrayOf(
            R.string.tab_text_1,
            R.string.tab_text_2
        )
    }
}

This overrides the two required methods createFragment(), which creates Fragment instances, and getItemCount() which returns the number of pages in the adpater.

There’s an additional method here named getPageTitle() which will return a title for each page. Those that are familiar with the original ViewPager‘s PagerAdapter may know of its getPageTitle() method which can be useful when hooking ViewPager up to a TabLayout. There is no equivalent method in FragmentStateAdapter but we’ll define the method anyway so that we can see how easy it is to hook up to TabLayout.

All that remains is our Activity which performs all of the wiring:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val sectionsPagerAdapter = SectionsPagerAdapter(this)
        val viewPager = findViewById(R.id.view_pager).apply {
            adapter = sectionsPagerAdapter
            orientation = ViewPager2.ORIENTATION_HORIZONTAL
        }
        val tabs: TabLayout = findViewById(R.id.tabs)
        TabLayoutMediator(tabs, viewPager, true) { tab, position ->
            tab.text = sectionsPagerAdapter.getPageTitle(position)
        }.attach()
    }
}

First we create an instance of our SectionsPagerAdapter, and then we obtain ViewPager2 instance from the layout. We then configure the ViewPager2 instance by setting the adapter to our SectionsPagerAdapter instance, and then setting the orientation to horizontal.

This is pretty standard RecyclerView setup and orientation works in the same way. Here we see another nice advantage of having ViewPager2 built around RecyclerView – we can have different orientations of ViewPager2 which was not possible with the original ViewPager. One thing that the more observant reader may have spotted is that we haven’t set a layout manager for the RecyclerView. ViewPager2 automatically sets a LinearLayoutManager for us because it’s going to be by far the most common use case. However, it is possible to set a different one if required, so it’s possible to get some really funky ViewPager behaviours by using different layout managers.

The final thing that we need to set up is wiring up the ViewPager2 to our TabLayout. While TabLayout has a couple of setupWithViewPager() methods, it is completely agnostic of ViewPager2 so we cannot use those methods. Instead the setup is done by a TabLayoutMediator instance. TabLayoutMediator is part of the ViewPager2 library, and does all of the necessary work for us. The arguments are the TabLayout instance, then the ViewPager2 instance, a boolean which controls whether the the tabs will be automatically updated when the ViewPager2‘s adapter is updated, and finally an OnConfiguarionCallback instance which is implemented here as a lambda.

OnConfigurationCallback is called to configure the tab representing each page within the ViewPager2. Within this we set the text of the tab using the getPageTitle() method that we added to the SectionsPagerAdapter.

And we’re done. If we run this we get the behaviour that we and, more importantly, our users are familiar with. We can both swipe between pages, and also tap the tabs to change pages:

Although we’ve just mimicked the behaviour of the original ViewPager here, it will now be much easier to modify that behaviour. An obvious thing here would be how easy it would to convert this to a vertical ViewPager2 – just a single line change to alter the orientation. That certainly would have required significantly more effort in the original ViewPager. But we could also utilise RecyclerView‘s ItemDecoration to add dividers and suchlike between the pages. Using RecyclerView allows us plenty of scope for customisation!

We’ve only covered the basics of what is possible here but those that are experienced with RecyclerView should already have an idea of how powerful this new approach to ViewPager really is.

That it for this introduction to ViewPager2. However the next article will cover a very different topic, yet will build upon the code that we’ve used here to get a really nice visual effect.

The source code for this article is available here.

© 2019, Mark Allison. All rights reserved.

Copyright © 2019 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

3 Comments

  1. Thanks for article!
    But It’s look strange the that the PageFragment knows about his position in Viewpager, not isn’t?

  2. But it doesn’t. There is no logic based around the value of sectionNumber. It uses whatever value is provided to it during construction to create the string that is displayed, but it is the ViewModel which contains the logic for creating that string. The value of sectionNumber does not affect the PageFagment at all. In fact PageFragment does not directly access sectionNumber, it just passes whatever value is in the ARG_SECTION_NUMBER argument to the ViewModel.

    We could probably bypass the use of an argument by injecting the ViewModel already initialised, but this code is to demonstrate how to implement ViewPager2, and I didn’t want to confuse things by adding unnecessary complexity which wasn’t related to the subject of the article.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.