Introduction
In Android, tabs are populated using Fragments by combining TabLayout with ViewPager2 and a FragmentStateAdapter. Each tab corresponds to a Fragment that defines its own layout and logic. The adapter maps tab positions to Fragment instances, and TabLayoutMediator connects the tab titles to the ViewPager pages. This pattern keeps each tab's UI isolated in its own Fragment, making the code modular and the lifecycle manageable.
Dependencies
1// build.gradle (app)
2dependencies {
3 implementation 'com.google.android.material:material:1.11.0'
4 implementation 'androidx.viewpager2:viewpager2:1.0.0'
5 implementation 'androidx.fragment:fragment-ktx:1.6.2'
6}
Activity Layout
1<!-- activity_main.xml -->
2<LinearLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:orientation="vertical">
8
9 <com.google.android.material.tabs.TabLayout
10 android:id="@+id/tabLayout"
11 android:layout_width="match_parent"
12 android:layout_height="wrap_content"
13 app:tabMode="fixed"
14 app:tabGravity="fill" />
15
16 <androidx.viewpager2.widget.ViewPager2
17 android:id="@+id/viewPager"
18 android:layout_width="match_parent"
19 android:layout_height="0dp"
20 android:layout_weight="1" />
21
22</LinearLayout>
TabLayout renders the tab bar. ViewPager2 handles swiping between pages and hosts the fragments.
Fragment Adapter
1import androidx.fragment.app.Fragment
2import androidx.fragment.app.FragmentActivity
3import androidx.viewpager2.adapter.FragmentStateAdapter
4
5class TabPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
6
7 override fun getItemCount(): Int = 3
8
9 override fun createFragment(position: Int): Fragment {
10 return when (position) {
11 0 -> HomeFragment()
12 1 -> SearchFragment()
13 2 -> ProfileFragment()
14 else -> HomeFragment()
15 }
16 }
17}
FragmentStateAdapter creates and manages Fragment instances. createFragment() is called once per position — the adapter caches fragments and handles their lifecycle.
Activity Setup
1import android.os.Bundle
2import androidx.appcompat.app.AppCompatActivity
3import com.google.android.material.tabs.TabLayout
4import com.google.android.material.tabs.TabLayoutMediator
5import androidx.viewpager2.widget.ViewPager2
6
7class MainActivity : AppCompatActivity() {
8
9 override fun onCreate(savedInstanceState: Bundle?) {
10 super.onCreate(savedInstanceState)
11 setContentView(R.layout.activity_main)
12
13 val tabLayout = findViewById<TabLayout>(R.id.tabLayout)
14 val viewPager = findViewById<ViewPager2>(R.id.viewPager)
15
16 // Set the adapter
17 viewPager.adapter = TabPagerAdapter(this)
18
19 // Connect TabLayout with ViewPager2
20 TabLayoutMediator(tabLayout, viewPager) { tab, position ->
21 tab.text = when (position) {
22 0 -> "Home"
23 1 -> "Search"
24 2 -> "Profile"
25 else -> "Tab $position"
26 }
27 }.attach()
28 }
29}
TabLayoutMediator synchronizes the tab selection with the ViewPager page position and sets tab titles via the lambda.
Tab Fragments
1// HomeFragment.kt
2class HomeFragment : Fragment() {
3 override fun onCreateView(
4 inflater: LayoutInflater,
5 container: ViewGroup?,
6 savedInstanceState: Bundle?
7 ): View {
8 return inflater.inflate(R.layout.fragment_home, container, false)
9 }
10
11 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
12 super.onViewCreated(view, savedInstanceState)
13 // Initialize views and load data here
14 val textView = view.findViewById<TextView>(R.id.homeText)
15 textView.text = "Welcome Home"
16 }
17}
1<!-- fragment_home.xml -->
2<FrameLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 android:layout_width="match_parent"
5 android:layout_height="match_parent"
6 android:padding="16dp">
7
8 <TextView
9 android:id="@+id/homeText"
10 android:layout_width="wrap_content"
11 android:layout_height="wrap_content"
12 android:layout_gravity="center"
13 android:textSize="24sp" />
14
15</FrameLayout>
Passing Data to Tab Fragments
1class TabPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
2
3 override fun getItemCount(): Int = 3
4
5 override fun createFragment(position: Int): Fragment {
6 val fragment = ContentFragment()
7 fragment.arguments = Bundle().apply {
8 putInt("tab_position", position)
9 putString("tab_title", getTabTitle(position))
10 }
11 return fragment
12 }
13
14 private fun getTabTitle(position: Int): String = when (position) {
15 0 -> "Home"
16 1 -> "Search"
17 2 -> "Profile"
18 else -> "Unknown"
19 }
20}
21
22// ContentFragment reads its arguments
23class ContentFragment : Fragment() {
24 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
25 super.onViewCreated(view, savedInstanceState)
26 val position = arguments?.getInt("tab_position") ?: 0
27 val title = arguments?.getString("tab_title") ?: "Unknown"
28 view.findViewById<TextView>(R.id.text).text = "Tab $position: $title"
29 }
30}
Use Bundle arguments to pass data to fragments. Never pass data through constructor parameters — Android recreates fragments using the no-argument constructor.
Tabs with Icons
1TabLayoutMediator(tabLayout, viewPager) { tab, position ->
2 when (position) {
3 0 -> {
4 tab.text = "Home"
5 tab.setIcon(R.drawable.ic_home)
6 }
7 1 -> {
8 tab.text = "Search"
9 tab.setIcon(R.drawable.ic_search)
10 }
11 2 -> {
12 tab.text = "Profile"
13 tab.setIcon(R.drawable.ic_profile)
14 }
15 }
16}.attach()
17
18// Icon-only tabs (no text)
19tabLayout.tabIconTint = null // preserve original icon colors
Using Fragment from a Parent Fragment
1// If your tabs are inside another Fragment (not an Activity)
2class ParentFragment : Fragment() {
3
4 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
5 super.onViewCreated(view, savedInstanceState)
6 val viewPager = view.findViewById<ViewPager2>(R.id.viewPager)
7 val tabLayout = view.findViewById<TabLayout>(R.id.tabLayout)
8
9 // Use childFragmentManager, not activity's
10 viewPager.adapter = ChildTabAdapter(this)
11
12 TabLayoutMediator(tabLayout, viewPager) { tab, position ->
13 tab.text = "Tab $position"
14 }.attach()
15 }
16}
17
18class ChildTabAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
19 override fun getItemCount(): Int = 3
20 override fun createFragment(position: Int): Fragment = ContentFragment()
21}
When tabs live inside a Fragment, pass the parent Fragment (not the Activity) to FragmentStateAdapter. This uses childFragmentManager so nested fragment lifecycles are managed correctly.
Common Pitfalls
Using the deprecated ViewPager instead of ViewPager2: The original ViewPager and FragmentPagerAdapter/FragmentStatePagerAdapter are deprecated. Use ViewPager2 with FragmentStateAdapter for proper lifecycle handling and RecyclerView-based performance.
Passing data through Fragment constructors: Android recreates Fragments using reflection with the no-argument constructor. Constructor parameters are lost. Always use Bundle arguments via fragment.arguments = Bundle().
Using activity!!.supportFragmentManager in nested fragments: When tabs are inside a Fragment, use childFragmentManager (by passing the Fragment to FragmentStateAdapter). Using the activity's fragment manager causes lifecycle issues and crashes on configuration changes.
Not calling .attach() on TabLayoutMediator: Forgetting .attach() means the tabs are not connected to the ViewPager. Tabs will not display titles and swiping will not update the selected tab.
Recreating fragments on every page switch: FragmentStateAdapter handles caching. Do not create new Fragment instances in onResume or reinitialize the adapter on every lifecycle event. Set the adapter once in onCreate or onViewCreated.
Summary
Use ViewPager2 + FragmentStateAdapter + TabLayoutMediator for tabbed interfaces
Each tab corresponds to a Fragment returned by createFragment() in the adapter
TabLayoutMediator connects tab titles/icons to ViewPager page positions
Pass data to fragments using Bundle arguments, never constructor parameters
Use childFragmentManager (pass the Fragment, not the Activity) when tabs are nested inside another Fragment
Call .attach() on TabLayoutMediator to activate the tab-page synchronization