Tabs are one of the key elements of navigation in mobile applications on Android. They allow you to divide content into logical blocks, improving the user experience and simplifying access to different sections of the application. Without them, it is difficult to imagine modern instant messengers, social networks or shopping applications, where tabs are used to switch between chats, news feeds or product categories.

In this article you will learn how to implement tabs in Android Studio from scratch: from basic setup TabLayout i ViewPager2 to advanced customization of appearance and animations. We'll look at current approaches (including Jetpack Compose for those working with a modern stack), typical beginner mistakes, and performance optimization. All code examples have been tested on the latest versions Android Gradle Plugin i Material Components.

1. Project preparation: dependencies and structure

Before adding tabs, make sure that your project is Android Studio configured correctly. The main dependencies for working with tabs are included in the library Material Components for Android, which is already included in most new projects. If it is not there, add it to build.gradle (Module: app):

dependencies {

implementation 'com.google.android.material:material:1.11.0'

implementation 'androidx.viewpager2:viewpager2:1.0.0'

}

After synchronizing the project, check that styles.xml is using the theme MaterialComponents (for example, Theme.MaterialComponents.DayNight). This guarantees the correct display of standard tab styles

  • ๐Ÿ“Œ Minimum requirements: Android 5.0 (API 21) and above. Older versions will require additional polyfills.
  • ๐Ÿ”ง Recommended structure: Create separate fragments (Fragment) for each tab - this will simplify their lifecycle management.
  • ๐Ÿš€ For Jetpack Compose: other dependencies - androidx.compose.material:material:1.6.0 and androidx.compose.foundation:foundation:1.6.0.
โš ๏ธ Attention: If you use AndroidX, avoid deprecated ViewPager - it has been replaced by ViewPager2which supports vertical scrolling and integrates better with RecyclerView.

2. Basic implementation of tabs with TabLayout and ViewPager2

The most common way to create tabs is a combination of TabLayout (for displaying the tabs themselves) and ViewPager2 (for managing content). Let's look at a step-by-step implementation:

Step 1. Markup in XML

Add the following elements to your activity_main.xml (or other layout):

<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/viewPager"

android:layout_width="match_parent"

android:layout_height="0dp"

android:layout_weight="1" />

Step 2. Adapter for ViewPager2

Create an adapter class that will manage fragments for each tab:

class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle)

: FragmentStateAdapter(fragmentManager, lifecycle) {

override fun getItemCount(): Int = 3 // Number of tabs

override fun createFragment(position: Int): Fragment {

return when (position) {

0 -> FirstFragment()

1 -> SecondFragment()

else -> ThirdFragment()

}

}

}

Step 3. Linking in Activity

In yours MainActivity.kt link TabLayout and ViewPager2:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_main)

val tabLayout = findViewById<TabLayout>(R.id.tabLayout)

val viewPager = findViewById<ViewPager2>(R.id.viewPager)

viewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)

TabLayoutMediator(tabLayout, viewPager) { tab, position ->

tab.text = "Tab ${position + 1}"

}.attach()

}

}

Synchronized dependencies in build.gradle|TabLayout and ViewPager2 added to XML|Created adapter for fragments|TabLayoutMediator links components|-->

This code will create 3 tabs with plain text. To dynamically change the number of tabs, just update getItemCount() in the adapter.

3. Customizing the appearance of tabs

Standard tabs from Material Design look neutral, but often you need to adapt them to the application design. Here are the key parameters for customization:

Parameter Description Example value
app:tabIndicatorColor Active tab indicator color @color/blue_500
app:tabTextColor Text color of inactive tabs @color/gray_600
app:tabSelectedTextColor Active tab text color @color/blue_700
app:tabBackground Tab background (for example, rounded) @drawable/tab_selector
app:tabMode Display mode (fixed or scrollable) scrollable

For a completely custom design (for example, tabs with icons or animation), create your own layout for tabs:

<com.google.android.material.tabs.TabLayout

...

app:tabContentStart="72">

<com.google.android.material.tabs.TabItem

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Home"

app:icon="@drawable/ic_home"/>

<com.google.android.material.tabs.TabItem

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Search"

app:icon="@drawable/ic_search"/>

</com.google.android.material.tabs.TabLayout>

๐Ÿ’ก

For tabs to occupy the entire width of the screen, use app:tabGravity="fill" i app:tabMode="fixed"For long names it is better tabMode="scrollable"

Important: when using icons in TabItem make sure that they are the same size (24x24 dp is recommended), otherwise the tabs will be of different widths.

4. Dynamically adding and removing tabs

In real applications, the number of tabs often changes dynamically (for example, in messengers when adding new chats). To do this:

Updating the adapter

Modify ViewPagerAdapterso that it accepts a list of fragments:

class ViewPagerAdapter(

fragmentManager: FragmentManager,

lifecycle: Lifecycle,

private val fragments: List<Fragment>

) : FragmentStateAdapter(fragmentManager, lifecycle) {

override fun getItemCount(): Int = fragments.size

override fun createFragment(position: Int): Fragment = fragments[position]

}

Adding a new tab

In MainActivity update the adapter and TabLayout when data changes:

private fun addNewTab(fragment: Fragment, title: String) {

val newFragments = (viewPager.adapter as ViewPagerAdapter).fragments.toMutableList()

newFragments.add(fragment)

viewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, newFragments)

TabLayoutMediator(tabLayout, viewPager) { tab, position ->

tab.text = if (position == newFragments.lastIndex) title else "Tab ${position + 1}"

}.attach()

}

โš ๏ธ Attention: When dynamic When updating tabs, always create a new instance TabLayoutMediator and call .attach(). Reusing the old mediator will result in duplicate tabs.

To delete a tab, use similar logic, but remove the element from the list fragments before updating the adapter.

Standard text|Tabs with icons|Custom design with animation|Dynamically resizable|Do not use tabs-->

5. Optimizing performance

Tabs with ViewPager2 can consume a lot of memory if not optimized. Here are the key recommendations:

  • ๐Ÿ”„ Fragment caching: By default ViewPager2 caches 1 fragment to the left and right of the current one. Change this via viewPager.offscreenPageLimit = 2 (for example, for 2 tabs on the sides).
  • ๐Ÿ—‘๏ธ Resource Cleanup: In onDestroyView() a fragment, free heavy objects (for example, Bitmap or MediaPlayer).
  • ๐Ÿ“‰ Lazy loading: Load data in the fragment only when it becomes visible (check userVisibleHint or use LifecycleObserver).
  • ๐Ÿ› ๏ธ Profiling: Use Android Profiler in Android Studio to monitor memory consumption when switching tabs.

Example of lazy loading of data in a fragment:

override fun setUserVisibleHint(isVisibleToUser: Boolean) {

super.setUserVisibleHint(isVisibleToUser)

if (isVisibleToUser && isAdded) {

loadData() // Load only when a tab is displayed

}

}

Why is ViewPager2 better than ViewPager?

ViewPager2 supports vertical scrolling, works better with RecyclerView, has built-in diffutil support for smooth animations, and is compatible with a wider range of Android versions.

6. Tabs in Jetpack Compose

If you use Jetpack Compose, the implementation of tabs is simplified thanks to the declarative approach. Main components:

  • ๐Ÿงฉ TabRow: Analog TabLayout to display tabs.
  • ๐Ÿ“„ Tab: A separate tab with text or an icon.
  • ๐Ÿ”„ pagerState: Manages the state of the current tab.

Code example:

@Composable

fun TabsScreen() {

val tabs = listOf("Home", "Search", "Profile")

var tabIndex by remember { mutableStateOf(0) }

Column {

TabRow(selectedTabIndex = tabIndex) {

tabs.forEachIndexed { index, title ->

Tab(

selected = tabIndex == index,

onClick = { tabIndex = index },

text = { Text(title) }

)

}

}

when (tabIndex) {

0 -> HomeScreen()

1 -> SearchScreen()

2 -> ProfileScreen()

}

}

}

For switching animation, use the library Accompanist (or built-in HorizontalPager in new versions of Compose):

implementation "com.google.accompanist:accompanist-pager:0.28.0"

This will allow you to add a swipe between tabs, as in classic ViewPager2.

7. Typical errors and their solutions

Even experienced developers encounter problems when working with tabs. Here are the most common errors and how to fix them:

Problem Cause Solution
Tabs are not updated when data changes The adapter is not notified about changes Use notifyDataSetChanged() or create a new adapter
When the screen is rotated, the current tab is reset State not saved ViewPager2 Save the position in onSaveInstanceState
Icons in tabs are cut off Wrong size of icons or padding Use 24x24 dp icons and adjust app:tabContentStart
Lags when swiping between tabs Too many fragments in memory Reduce offscreenPageLimit or optimize fragments

If tabs are not displayed correctly after returning from the background, check to see if FragmentStateAdapter is not being recreated again. The solution is to store the adapter as a class field Activity/Fragment and update only its data.

๐Ÿ’ก

Always test tabs on devices with different screen sizes. On small screens tabMode="scrollable" may not work intuitively - users will not notice that tabs can be scrolled.

8. Alternative approaches: BottomNavigation and Navigation Component

Tabs are not always the optimal solution. Consider alternatives:

  • ๐Ÿ“ฑ BottomNavigationView: Best suited for 3-5 main sections of the application (e.g. Home, Search, Profile). Tabs at the bottom of the screen are more convenient for navigation with your thumb.
  • ๐Ÿ—บ๏ธ Navigation Component: For complex navigation with nested stacks (for example, the "Chats" tab โ†’ "Dialog" โ†’ "User Profile").
  • ๐Ÿ”„ Combination of approaches: For example, BottomNavigation for main sections + tabs inside one of sections.

Implementation example BottomNavigationView:

<com.google.android.material.bottomnavigation.BottomNavigationView

android:id="@+id/bottomNav"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_gravity="bottom"

app:menu="@menu/bottom_nav_menu" />

Where bottom_nav_menu.xml:

<menu>

</menu>

Link navigation to NavController:

bottomNav.setupWithNavController(navController)
โš ๏ธ Attention: Avoid using and TabLayoutand BottomNavigationView at the same time - this overloads the interface. Choose one main navigation method.

FAQ: Frequently asked questions about tabs in Android

Is it possible to make animation when switching tabs?

Yes, use it for that ViewPager2.PageTransformer. Example of a simple animation:

viewPager.setPageTransformer { page, position ->

page.translationX = -position * page.width

page.alpha = 1 - abs(position)

}

For complex animations (for example, parallax effect), create a custom transformer.

How to make tabs with notification counters (badges)?

Use TabLayout together c BadgeDrawable:

val badge = tabLayout.getTabAt(0)?.orCreateBadge

badge.number = 5

badge.backgroundColor = Color.RED

To dynamically update the counter, call this code when the data changes (for example, when you receive a new message).

Why do tabs switch with a delay when you swipe?

This is due to the fact that ViewPager2 by default it waits for the swipe gesture to complete. To remove the delay, configure FakeDrag or use custom RecyclerView.ItemAnimator.

Also check if the main thread is blocked by a heavy operation in onPageSelected.

How to save the state of fragments when rotating the screen?

Use FragmentStateAdapter (it already saves the state of fragments) and make sure that your Activity does not recreate the adapter when turning. For additional reliability, save the current tab position in onSaveInstanceState:

override fun onSaveInstanceState(outState: Bundle) {

outState.putInt("current_tab", viewPager.currentItem)

super.onSaveInstanceState(outState)

}

Is it possible to use tabs without ViewPager2?

Yes, but this will require manual implementation of the content switching logic. For example, you can hide/show FrameLayout with different content when you click on TabLayout:

tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {

override fun onTabSelected(tab: TabLayout.Tab) {

when (tab.position) {

0 -> { frame1.visibility = View.VISIBLE; frame2.visibility = View.GONE }

1 -> { frame1.visibility = View.GONE; frame2.visibility = View.VISIBLE }

}

}

// ... other methods

})

However, this approach is less flexible and does not support swipe.