How to use TabLayout with ViewPager2 in Android

I want to use com.google.android.material.tabs.TabLayout component with Android's new ViewPager implementation androidx.viewpager2.widget.ViewPager2. However, the setupWithViewPager(..) method provided by TabLayout supports only the old ViewPager implementation. Is there a way to bind a TabLayout to a ViewPager2 component easily?


Solution 1:

You have to use this TabLayoutMediator that mimics tabLayout.setupWithViewPager() and sets up the ViewPager2 with Tablayout. Otherwise, you will have to write your own adapter that will combine both parties.

Its code will look like this in Kotlin

TabLayoutMediator(tabLayout, viewPager) { tab, position ->
  tab.text = tabTitles[position]
}.attach()

Solution 2:

No hacks, no extensions, no TabLayoutMediator

I am on implementation 'com.google.android.material:material:1.2.0-alpha02' and do the following without needing the TabLayoutMediator. Instead, I link the TabLayout with the ViewPager2 using the method described here. I've also added a working example to github here. I think I've minimized the solution to a minimal working example. I'll explain the important bits.

Adding the elements to the template

First we'll need to add the TabLayout and ViewPager2 to the layout. I've placed them inside a LinearLayout and CoordinatorLayout here, but you can do whatever you like of course.

<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.google.android.material.tabs.TabLayout
                android:id="@+id/tabLayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
            <androidx.viewpager2.widget.ViewPager2
                android:id="@+id/pager"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>

        </LinearLayout>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

Connecting an adapter to the viewpager

So the adapter is in charge of supplying the correct fragments to the activity. You'll have to extend FragmentStateAdapter which I've done very simply as below (it's a private class because it's declared within my MainActivity.java here):

    private class ViewStateAdapter extends FragmentStateAdapter {

        public ViewStateAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) {
            super(fragmentManager, lifecycle);
        }

        @NonNull
        @Override
        public Fragment createFragment(int position) {
            // Hardcoded in this order, you'll want to use lists and make sure the titles match
            if (position == 0) {
                return new BarFragment();
            }
            return new FooFragment();
        }

        @Override
        public int getItemCount() {
            // Hardcoded, use lists
            return 2;
        }
    }

I can then connect my own Adapter to the ViewPager as below:

FragmentManager fm = getSupportFragmentManager();
ViewStateAdapter sa = new ViewStateAdapter(fm, getLifecycle());
final ViewPager2 pa = findViewById(R.id.pager);
pa.setAdapter(sa);

I've added the fragments to my viewpager. (Because I hardcoded the Fragments in my adapter, you should use a list and something like an 'addFragment' method or something)

The TabLayout

Then with

TabLayout tabLayout = findViewById(R.id.tabLayout);
tabLayout.addTab(tabLayout.newTab().setText("Bar"));
tabLayout.addTab(tabLayout.newTab().setText("Foo"));

I add two tabs to my TabLayout, showing the titles but not letting me switch to the fragments yet.

Connecting TabLayout to Adapter

tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                pa.setCurrentItem(tab.getPosition());
            }

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

            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {

            }
        });

This should be pretty straightforward. User clicks on a tab, I get the position in my callback and I simply set the adapter's current item to that position.

Change Tab when swiping

Finally we couple back when the user swipes the fragment to set the correct tab item as selected

pa.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
    @Override
    public void onPageSelected(int position) {
        tabLayout.selectTab(tabLayout.getTabAt(position));
    }
});

Solution 3:

UPDATE

check this Create swipe views with tabs using ViewPager2

Here is the Updated answer How to use TabLayout with ViewPager2 in Android

Now we no need to create a class from TabLayoutMediator

Use below dependencies

implementation 'com.google.android.material:material:1.1.0-alpha08'
implementation 'androidx.viewpager2:viewpager2:1.0.0-beta02'

SAMPLE CODE

XMl layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:layout_scrollFlags="scroll|enterAlways"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

        <com.google.android.material.tabs.TabLayout
                android:id="@+id/tabs"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/viewpager"
            app:layout_anchor="@id/tabs"
            app:layout_anchorGravity="bottom"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
    />


</androidx.coordinatorlayout.widget.CoordinatorLayout>

Activity

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import com.google.android.material.tabs.TabLayoutMediator

import com.google.android.material.tabs.TabLayout


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

//        setSupportActionBar(toolbar)
        viewpager.adapter = AppViewPagerAdapter(supportFragmentManager, lifecycle)

        TabLayoutMediator(tabs, viewpager, object : TabLayoutMediator.OnConfigureTabCallback {
            override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
                // Styling each tab here
                tab.text = "Tab $position"
            }
        }).attach()


    }
}

UPDATE

If your using implementation 'com.google.android.material:material:1.1.0-alpha10' then use below code

        TabLayoutMediator(tabs, viewpage,
        TabLayoutMediator.TabConfigurationStrategy { tab, position ->
            when (position) {
                0 -> { tab.text = "TAB ONE"}
                1 -> { tab.text = "TAB TWO"}
            }
        }).attach()

OUTPUT

enter image description here

Solution 4:

Initialize the TabLayoutMediator object with an object of TabLayout, ViewPager2 , autoRefresh -- boolean type, and an object of OnConfigurationChangeCallback.

TabLayoutMediator tabLayoutMediator = new TabLayoutMediator(tabLayout, viewPager2, true, new TabLayoutMediator.OnConfigureTabCallback() {
  @Override
  public void onConfigureTab(TabLayout.Tab tab, int position) {
    // position of the current tab and that tab  
  }
});

Finally just call attach() to the TabLayoutMediator object to wire up the tablayout to the viewpager :-

 tabLayoutMediator.attach();

autoRefresh - key if set to true -- ( By default its set to true )

RECREATES all the tabs of the tabLayout if notifyDataSetChanged is called to the viewpager adapter.

Use the contents of TabLayoutMediator.java

Solution 5:

You can use Kotlin extension function:

fun TabLayout.setupWithViewPager(viewPager: ViewPager2, labels: List<String>) {

    if (labels.size != viewPager.adapter?.itemCount)
        throw Exception("The size of list and the tab count should be equal!")

    TabLayoutMediator(this, viewPager,
        TabLayoutMediator.TabConfigurationStrategy { tab, position ->
            tab.text = labels[position]
        }).attach()
}

And call it:

 tabLayout.setupWithViewPager(viewPager, listOf("Tab A", "Tab B"))