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.0andandroidx.compose.foundation:foundation:1.6.0.
โ ๏ธ Attention: If you use AndroidX, avoid deprecatedViewPager- it has been replaced byViewPager2which supports vertical scrolling and integrates better withRecyclerView.
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.TabLayoutandroid: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 instanceTabLayoutMediatorand 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
ViewPager2caches 1 fragment to the left and right of the current one. Change this viaviewPager.offscreenPageLimit = 2(for example, for 2 tabs on the sides). - ๐๏ธ Resource Cleanup: In
onDestroyView()a fragment, free heavy objects (for example,BitmaporMediaPlayer). - ๐ Lazy loading: Load data in the fragment only when it becomes visible (check
userVisibleHintor useLifecycleObserver). - ๐ ๏ธ 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: AnalogTabLayoutto display tabs. - ๐
Tab: A separate tab with text or an icon. - ๐
pagerState: Manages the state of the current tab.
Code example:
@Composablefun 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,
BottomNavigationfor main sections + tabs inside one of sections.
Implementation example BottomNavigationView:
<com.google.android.material.bottomnavigation.BottomNavigationViewandroid: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 andTabLayoutandBottomNavigationViewat 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)?.orCreateBadgebadge.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.