Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
📋 Screens, Lists & Navigation  ·  Lesson 28

TabLayout & ViewPager2

Tabs that swipe

TabLayout draws the tab titles. ViewPager2 holds the pages and lets the user swipe. TabLayoutMediator keeps both in sync.

9:41▮▮ 85%☰Student Diary⋮AllPendingDoneMaths homeworkHighBuy notebookDone9:41▮▮ 85%☰Student Diary⋮AllPendingDoneMaths homeworkHighPhysics revisionMedium
Swipe left → the Pending tab is selected
TabLayouttitlesTabLayoutMediatorsyncViewPager2pagesFragmentStateAdaptercreates fragments
Four pieces

Step 1: Layout

XML – res/layout/fragment_notes.xml
<?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">

    <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/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
</LinearLayout>

Step 2: FragmentStateAdapter

Kotlin
class CoursePagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
    override fun getItemCount() = 3
    override fun createFragment(position: Int): Fragment = when (position) {
        0 -> SyllabusFragment()
        1 -> ReviewsFragment()
        else -> FaqFragment()
    }
}

Step 3: TabLayoutMediator

Kotlin
val titles = listOf("Syllabus", "Reviews", "FAQ")
binding.viewPager.adapter = CoursePagerAdapter(this)
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
    tab.text = titles[position]
}.attach()

// listen for page changes
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    override fun onPageSelected(position: Int) {
        Log.d("Tabs", "Now on ${titles[position]}")
    }
})

Real code: Student Diary tabs

One NoteListFragment class is reused for all three tabs with a different filter argument:

Kotlin – ui/notes/NotesFragment.kt
package com.codingeasily.studentdiary.ui.notes

import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.codingeasily.studentdiary.R
import com.codingeasily.studentdiary.databinding.FragmentNotesBinding
import com.codingeasily.studentdiary.ui.NoteFilter
import com.codingeasily.studentdiary.ui.NoteViewModel
import com.google.android.material.tabs.TabLayoutMediator

/** Home screen: 3 tabs (All / Pending / Done) in a ViewPager2 + a search box in the toolbar. */
class NotesFragment : Fragment() {

    private var _binding: FragmentNotesBinding? = null
    private val binding get() = _binding!!
    private val viewModel: NoteViewModel by activityViewModels()

    private val tabs = listOf(
        NoteFilter.ALL to R.string.tab_all,
        NoteFilter.PENDING to R.string.tab_pending,
        NoteFilter.DONE to R.string.tab_done
    )

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _binding = FragmentNotesBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.viewPager.adapter = object : FragmentStateAdapter(this) {
            override fun getItemCount() = tabs.size
            override fun createFragment(position: Int) = NoteListFragment.newInstance(tabs[position].first)
        }
        TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
            tab.setText(tabs[position].second)
        }.attach()

        // Toolbar menu that belongs to this screen only
        requireActivity().addMenuProvider(object : MenuProvider {
            override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
                menuInflater.inflate(R.menu.menu_notes, menu)
                val searchView = menu.findItem(R.id.action_search).actionView as SearchView
                searchView.queryHint = getString(R.string.search_hint)
                searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                    override fun onQueryTextSubmit(query: String?) = true
                    override fun onQueryTextChange(newText: String?): Boolean {
                        viewModel.setSearchQuery(newText.orEmpty())
                        return true
                    }
                })
            }
            override fun onMenuItemSelected(menuItem: MenuItem) = false
        }, viewLifecycleOwner, Lifecycle.State.RESUMED)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        viewModel.setSearchQuery("")
        _binding = null
    }
}

Icons, badges and modes

Kotlin
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
    tab.text = titles[position]
    tab.setIcon(icons[position])                 // R.drawable.ic_…
}.attach()

binding.tabLayout.getTabAt(1)?.orCreateBadge?.number = 12   // "Reviews 12"
XML – many tabs that scroll
<com.google.android.material.tabs.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:tabMode="scrollable"
    app:tabGravity="start" />

Onboarding slider with views

ViewPager2 also accepts a normal RecyclerView adapter — perfect for welcome slides.

Kotlin
data class Slide(val emoji: String, val title: String, val text: String)

class SlideAdapter(private val slides: List<Slide>) : RecyclerView.Adapter<SlideAdapter.VH>() {
    class VH(val b: ItemSlideBinding) : RecyclerView.ViewHolder(b.root)
    override fun onCreateViewHolder(p: ViewGroup, t: Int) =
        VH(ItemSlideBinding.inflate(LayoutInflater.from(p.context), p, false))   // item root: match_parent
    override fun onBindViewHolder(h: VH, i: Int) {
        h.b.tvEmoji.text = slides[i].emoji
        h.b.tvTitle.text = slides[i].title
        h.b.tvText.text = slides[i].text
    }
    override fun getItemCount() = slides.size
}

// Activity
val slides = listOf(
    Slide("📝", "Write notes", "Homework, revision and exam notes in one place"),
    Slide("⏰", "Never forget", "Get a reminder before every deadline"),
    Slide("📊", "Track progress", "See how much you have completed")
)
binding.pager.adapter = SlideAdapter(slides)
TabLayoutMediator(binding.dots, binding.pager) { _, _ -> }.attach()   // TabLayout as dots
binding.btnNext.setOnClickListener {
    if (binding.pager.currentItem < slides.lastIndex) binding.pager.currentItem++ else finishOnboarding()
}
9:41▮▮ 85%📝Write notesHomework, revision and examnotes●○○NEXT9:41▮▮ 85%⏰Never forgetA reminder before every deadline○●○NEXT
Onboarding: ViewPager2 + TabLayout dots

Common mistakes

  • Forgetting .attach() on the mediator → tabs show no titles.
  • ViewPager2 page layouts with wrap_content → crash "Pages must fill the whole ViewPager2"; use match_parent.
  • Keeping references to page fragments in a list → they are recreated; communicate through a shared ViewModel.

Summary

  • TabLayout + ViewPager2 + FragmentStateAdapter + TabLayoutMediator.
  • Reuse one fragment class with arguments for similar tabs.
  • ViewPager2 with a RecyclerView adapter for sliders and onboarding.
✅ Practice

Build a Course screen with three tabs (Syllabus, Reviews, FAQ) and a badge showing the review count. Add a 3-slide onboarding screen that opens only on first launch (save a flag in SharedPreferences).

Frequently Asked Questions

ViewPager or ViewPager2?
Use ViewPager2. It is built on RecyclerView, supports right-to-left, vertical paging and DiffUtil, and is the maintained version.
Inside a Fragment, should I pass this or requireActivity() to FragmentStateAdapter?
Pass this (the parent fragment). The pages then become child fragments and are cleaned up with the parent.
How do I open a specific tab from code?
Call viewPager.setCurrentItem(index, true). The TabLayout follows automatically because of the mediator.
← Android Tutorial (Kotlin)
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicSharedPreferences & DataStore Your progress: 28 / 43 (65%) · Lesson 29 / 43 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free