In today’s tutorial, we will learn how to integrate Material Top Tab Navigator in Compose Multiplatform KMP KMM for both Android & ios platforms.
Material Top Tab Navigator in Compose Multiplatform (KMP/KMM):
The Material style top tab navigator is a Material theme top tab component. It displays multiple tabs at the top of the device screen. It allows the user to switch between Screens by tapping on the Top tabs. In KMM – KMP, we will use the TabRow composable component to show the Top tab bar and to render each tab, we will use the Tab composable component.
Top Tab Icons:
We are creating 3 Tabs: Home, List and Settings. You can see the icons below. I am using Google Material Icons. You can download these icons and use them in your project.
1. Configure Icons in Project:
1. Please download the Top tab icons from above and put them into:
APP -> composeApp -> src -> commonMain -> composeResource -> drawable folder.
2. Now we have to rebuild and then clean the project.
Note: The drawable folder works as a common resource folder for both Android & ios platforms.
2. Creating 3 Screens for Top tabs:
1. Creating a package named screens. In this folder, we will put all 3 Top tabs screens.
2. Creating Home.kt file. This is our home screen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package com.app.test.screens import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @Composable fun HomeScreen() { Column( modifier = Modifier .padding(12.dp) .fillMaxWidth() .fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "Home Screen", style = MaterialTheme.typography.h4, color = Color.Black, textAlign = TextAlign.Center ) } } |
3. Creating List.kt file. This is our List screen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package com.app.test.screens import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @Composable fun ListScreen() { Column( modifier = Modifier .padding(12.dp) .fillMaxWidth() .fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "List Screen", style = MaterialTheme.typography.h4, color = Color.Black, textAlign = TextAlign.Center ) } } |
4. Creating Settings.kt file. This is our Settings screen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package com.app.test.screens import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @Composable fun SettingsScreen() { Column( modifier = Modifier .padding(12.dp) .fillMaxWidth() .fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = "Settings Screen", style = MaterialTheme.typography.h4, color = Color.Black, textAlign = TextAlign.Center ) } } |
3. Start coding for the app:
1. Open your project’s main App.kt file and start coding for material style top tab navigator integration.
2. Defining a State named selectedTab. It is used to store the selected tab index.
1 |
var selectedTab by remember { mutableStateOf(0) } |
3. Creating 5 variables:
- iconSize: Define a reusable Modifier for Icon sizing.
- activeTabTextColor: Top tab navigator active tab text color.
- inactiveTabTextColor: Top tab inactive tab text color.
- tabBackgroundColor: Top tab background color.
- activeIndicatorColor: Selected tab active indicator color.
1 2 3 4 5 |
val iconSize = Modifier.size(width = 30.dp, height = 30.dp) val activeTabTextColor = Color(0xFFFFFFFF) val inactiveTabTextColor = Color(0xFFB2DFDB) val tabBackgroundColor = Color(0xff00BFA5) val activeIndicatorColor = Color(0xFFC51162) |
4. Creating a tab list. Here we would define the Top tab navigator titles.
1 2 3 |
val tabs = remember { listOf("Home", "List", "Settings") } |
5. Creating a Material Top Tab Navigator in Compose Multiplatform:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
Scaffold( topBar = { TabRow( selectedTabIndex = selectedTab, backgroundColor = tabBackgroundColor, modifier = Modifier.height(80.dp), indicator = { tabPositions -> TabRowDefaults.Indicator( modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTab]), color = activeIndicatorColor, )} ) { tabs.forEachIndexed { index, tab -> val isActive = selectedTab == index val currentIcon = when (index) { 0 -> painterResource(Res.drawable.home_icon) 1 -> painterResource(Res.drawable.list) 2 -> painterResource(Res.drawable.settings) else -> throw IllegalArgumentException("Unknown tab: $tab") } Tab( selected = isActive, onClick = { selectedTab = index }, text = { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .padding(horizontal = 8.dp, vertical = 1.dp) ) { Icon( painter = currentIcon, contentDescription = tab, modifier = iconSize, tint = if (isActive) activeTabTextColor else inactiveTabTextColor ) Spacer(modifier = Modifier.height(5.dp)) Text( text = tab, fontSize = 18.sp, color = if (isActive) activeTabTextColor else inactiveTabTextColor ) } } ) } } } ) { AnimatedContent( targetState = selectedTab, transitionSpec = { fadeIn() with fadeOut() } ) { targetState -> when (targetState) { 0 -> HomeScreen() 1 -> ListScreen() 2 -> SettingsScreen() } } } |
Code explanation:
- selectedTabIndex = selectedTab: Highlights the currently selected tab based on the selectedTab state.
- backgroundColor = tabBackgroundColor: Sets the TabRow background to teal.
- modifier = Modifier.height(80.dp): Sets the TabRow height to 80 dp, accommodating icons and text.
- indicator: Customizes the underline indicator for the selected tab.
- TabRowDefaults.Indicator: Default indicator composable.
- Modifier.tabIndicatorOffset(tabPositions[selectedTab]): Positions the indicator under the selected tab.
- color = activeIndicatorColor: Sets the indicator color to pink.
- tabs.forEachIndexed { index, tab -> … }: Iterates over the tabs list to create tabs for each item.
- painterResource: Loads drawable resources for KMP (from Res.drawable).
- selected = isActive: Highlights the tab if isActive (selectedTab == index).
- onClick = { selectedTab = index }: Updates selectedTab to the clicked tab’s index, triggering a screen change.
- text: Custom content for the tab (a Column with an icon and text).
- horizontalAlignment = Alignment.CenterHorizontally: Center the icon and text.
- targetState = selectedTab: Triggers animation when selectedTab changes.
- transitionSpec = { fadeIn() with fadeOut() }: Fades in the new screen and fades out the previous one.
Complete source code of App.kt file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
package com.app.test import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.with import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Tab import androidx.compose.material.TabRow import androidx.compose.material.TabRowDefaults import androidx.compose.material.TabRowDefaults.tabIndicatorOffset import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.app.test.screens.HomeScreen import com.app.test.screens.ListScreen import com.app.test.screens.SettingsScreen import org.jetbrains.compose.resources.painterResource import testapp.composeapp.generated.resources.Res import testapp.composeapp.generated.resources.home_icon import testapp.composeapp.generated.resources.list import testapp.composeapp.generated.resources.settings @OptIn(ExperimentalAnimationApi::class) @Composable fun App() { MaterialTheme { var selectedTab by remember { mutableStateOf(0) } val iconSize = Modifier.size(width = 30.dp, height = 30.dp) val activeTabTextColor = Color(0xFFFFFFFF) val inactiveTabTextColor = Color(0xFFB2DFDB) val tabBackgroundColor = Color(0xff00BFA5) val activeIndicatorColor = Color(0xFFC51162) val tabs = remember { listOf("Home", "List", "Settings") } Scaffold( topBar = { TabRow( selectedTabIndex = selectedTab, backgroundColor = tabBackgroundColor, modifier = Modifier.height(80.dp), indicator = { tabPositions -> TabRowDefaults.Indicator( modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTab]), color = activeIndicatorColor, )} ) { tabs.forEachIndexed { index, tab -> val isActive = selectedTab == index val currentIcon = when (index) { 0 -> painterResource(Res.drawable.home_icon) 1 -> painterResource(Res.drawable.list) 2 -> painterResource(Res.drawable.settings) else -> throw IllegalArgumentException("Unknown tab: $tab") } Tab( selected = isActive, onClick = { selectedTab = index }, text = { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .padding(horizontal = 8.dp, vertical = 1.dp) ) { Icon( painter = currentIcon, contentDescription = tab, modifier = iconSize, tint = if (isActive) activeTabTextColor else inactiveTabTextColor ) Spacer(modifier = Modifier.height(5.dp)) Text( text = tab, fontSize = 18.sp, color = if (isActive) activeTabTextColor else inactiveTabTextColor ) } } ) } } } ) { AnimatedContent( targetState = selectedTab, transitionSpec = { fadeIn() with fadeOut() } ) { targetState -> when (targetState) { 0 -> HomeScreen() 1 -> ListScreen() 2 -> SettingsScreen() } } } } } |
Screenshot on an Android device:
Screenshot on an iOS device: