How to Build Instagram-Style Bottom Navigation in Android

- Use
BottomNavigationView for three to five top-level destinations.- Give each bottom tab its own nested navigation graph.
- Connect the bottom bar using
setupWithNavController().- Jetpack Navigation automatically saves and restores each tab’s back stack.
- Use standard navigation actions to open screens within a tab.
- Store important UI data in a
ViewModel or SavedStateHandle.- Avoid manually coordinating fragments with repeated
add(), hide(), and show() transactions.Bottom navigation becomes difficult when each tab has its own navigation flow. Users expect to switch between Home, Search, News, and Profile without losing the screen or content they previously opened.
In our experience, manually coordinating add(), replace(), hide(), and show() transactions can quickly cause duplicate fragments, lost UI state, and unpredictable Back behaviour.
Jetpack Navigation provides a cleaner solution. When connected to BottomNavigationView, it can automatically save and restore a separate back stack for every top-level tab.
What Is Instagram-Style Bottom Navigation?
Instagram-style bottom navigation is a navigation pattern in which each bottom tab maintains its own screen history and UI state. Users can switch between tabs without losing their position within any tab.
For example, imagine an application with five tabs:
- Home
- Search
- Create
- News
- Profile
A user might follow this path:
Home → Post Details → Comments
Profile → Edit Profile
Home → Returns to Comments
When the user switches from Home to Profile, the Home tab’s back stack is saved. Returning to Home restores the Comments screen instead of restarting from the main Home screen.
Each tab therefore maintains a separate back stack:
Home: Home → Post Details → Comments
Search: Search
Create: Create
News: News → Notification Details
Profile: Profile → Edit Profile
Only one stack is visible at a time, but the navigation state of the other tabs remains available. This creates the continuous experience associated with applications such as Instagram.
The system Back action should move backwards through the currently selected tab:
Comments → Post Details → Home
It should not unexpectedly open a destination belonging to another tab or reset every tab to its starting screen.
What Will We Build?
By the end of this guide, the application will:
- Keep the bottom navigation bar visible across tab screens.
- Maintain an independent back stack for every tab.
- Restore the last-opened destination when a user returns to a tab.
- Support nested navigation inside Home and News.
- Work with Android’s system and gesture-based Back actions.
- Preserve important screen state across configuration changes.
Why Manual Fragment Transactions Become Difficult
A basic implementation may replace the current fragment whenever a bottom tab is selected:
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, HomeFragment())
.commit()
This works for a small prototype. However, every replacement can create a new fragment instance unless the application separately saves and restores its state.
A more advanced implementation might keep root fragments alive using add(), hide(), and show(). The activity must then manually coordinate:
- The currently active fragment
- Child fragment back stacks
- Back-button behaviour
- Duplicate tab selections
- Fragment lifecycle states
- Configuration changes
- Process recreation
- Saved UI state
We have found that problems become more visible when a screen inside one tab opens another fragment. Switching tabs may appear to work, but pressing Back can reveal that the selected tab and displayed fragment stack are no longer synchronized.
Jetpack Navigation handles these responsibilities through a NavController.
Why Use BottomNavigationView Instead of TabLayout?
Older Android implementations sometimes used TabLayout because early versions of BottomNavigationView provided less control. That reasoning is no longer applicable to most modern Android applications.
BottomNavigationView is specifically designed for three to five top-level destinations. It provides:
- Material-compliant bottom navigation
- Selected and unselected item states
- Accessibility support
- Badge support
- Navigation Component integration
- Automatic multiple-back-stack handling
- State restoration when switching tabs
TabLayout is better suited to closely related or swipeable pages, such as categories displayed through ViewPager2.
For persistent top-level destinations such as Home, Search, News, and Profile, BottomNavigationView communicates the application hierarchy more accurately.
Step 1: Add the Navigation Dependencies
Add the Navigation Fragment and Navigation UI libraries to the app module:
dependencies {
implementation("androidx.navigation:navigation-fragment-ktx:2.9.8")
implementation("androidx.navigation:navigation-ui-ktx:2.9.8")
}
The project should also include Material Components for BottomNavigationView and ConstraintLayout for the activity layout.
Step 2: Create the Bottom Navigation Menu
Create res/menu/bottom_navigation_menu.xml:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/home_graph"
android:icon="@drawable/ic_home"
android:title="@string/home" />
<item
android:id="@+id/search_graph"
android:icon="@drawable/ic_search"
android:title="@string/search" />
<item
android:id="@+id/create_graph"
android:icon="@drawable/ic_add"
android:title="@string/create" />
<item
android:id="@+id/news_graph"
android:icon="@drawable/ic_notifications"
android:title="@string/news" />
<item
android:id="@+id/profile_graph"
android:icon="@drawable/ic_profile"
android:title="@string/profile" />
</menu>
The menu item IDs must match the IDs of the corresponding nested navigation graphs. If they differ, setupWithNavController() cannot map each tab to the correct graph.
Step 3: Add the NavHost and BottomNavigationView
Create the activity layout:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:navGraph="@navigation/main_navigation"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:menu="@menu/bottom_navigation_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
FragmentContainerView hosts the active destination. The bottom navigation remains outside the container, so it stays visible while fragments change.
Automate Your Android Workflow
We build Android Studio plugins that cut manual effort, improve speed, and make your development process seamless.
Setting app:defaultNavHost="true" allows the NavHostFragment to handle Android’s Back action.
Step 4: Create a Nested Graph for Each Tab
Create res/navigation/main_navigation.xml:
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_navigation"
app:startDestination="@id/home_graph">
<navigation
android:id="@+id/home_graph"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.example.app.home.HomeFragment"
android:label="Home">
<action
android:id="@+id/action_home_to_post_details"
app:destination="@id/postDetailsFragment" />
</fragment>
<fragment
android:id="@+id/postDetailsFragment"
android:name="com.example.app.home.PostDetailsFragment"
android:label="Post details">
<action
android:id="@+id/action_post_details_to_comments"
app:destination="@id/commentsFragment" />
</fragment>
<fragment
android:id="@+id/commentsFragment"
android:name="com.example.app.home.CommentsFragment"
android:label="Comments" />
</navigation>
<navigation
android:id="@+id/search_graph"
app:startDestination="@id/searchFragment">
<fragment
android:id="@+id/searchFragment"
android:name="com.example.app.search.SearchFragment"
android:label="Search" />
</navigation>
<navigation
android:id="@+id/create_graph"
app:startDestination="@id/createFragment">
<fragment
android:id="@+id/createFragment"
android:name="com.example.app.create.CreateFragment"
android:label="Create" />
</navigation>
<navigation
android:id="@+id/news_graph"
app:startDestination="@id/newsFragment">
<fragment
android:id="@+id/newsFragment"
android:name="com.example.app.news.NewsFragment"
android:label="News">
<action
android:id="@+id/action_news_to_notification_details"
app:destination="@id/notificationDetailsFragment" />
</fragment>
<fragment
android:id="@+id/notificationDetailsFragment"
android:name="com.example.app.news.NotificationDetailsFragment"
android:label="Notification details" />
</navigation>
<navigation
android:id="@+id/profile_graph"
app:startDestination="@id/profileFragment">
<fragment
android:id="@+id/profileFragment"
android:name="com.example.app.profile.ProfileFragment"
android:label="Profile">
<action
android:id="@+id/action_profile_to_edit_profile"
app:destination="@id/editProfileFragment" />
</fragment>
<fragment
android:id="@+id/editProfileFragment"
android:name="com.example.app.profile.EditProfileFragment"
android:label="Edit profile" />
</navigation>
</navigation>
Each nested graph represents an independent tab flow.
The Home graph contains:
HomeFragment → PostDetailsFragment → CommentsFragment
The News graph contains:
NewsFragment → NotificationDetailsFragment
The Profile graph contains:
ProfileFragment → EditProfileFragment
The com.example.app class names illustrate the expected project structure. In an actual project, they correspond to the package containing each fragment.
Step 5: Connect BottomNavigationView to NavController
If the project uses View Binding, configure the navigation in MainActivity:
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.example.app.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
binding.bottomNavigation.setupWithNavController(navController)
}
}
That single setupWithNavController() call connects the bottom bar to the navigation graph.
Navigation Component 2.4.0 and later automatically saves the current stack and restores the previously selected tab’s stack. There is no need to manually add, replace, hide, or show the root fragments.
If View Binding is not already enabled, add it to the app module:
android {
buildFeatures {
viewBinding = true
}
}
Step 6: Navigate Within the Home Tab
Inside HomeFragment, open Post Details:
import androidx.navigation.fragment.findNavController
binding.openPostButton.setOnClickListener {
findNavController().navigate(
R.id.action_home_to_post_details
)
}
Inside PostDetailsFragment, open Comments:
binding.openCommentsButton.setOnClickListener {
findNavController().navigate(
R.id.action_post_details_to_comments
)
}
The Home stack now becomes:
HomeFragment → PostDetailsFragment → CommentsFragment
If the user switches to Profile and later returns to Home, the Comments screen is restored automatically.
Step 7: Navigate Within the Other Tabs
Inside NewsFragment:
import androidx.navigation.fragment.findNavController
binding.openNotificationButton.setOnClickListener {
findNavController().navigate(
R.id.action_news_to_notification_details
)
}
Inside ProfileFragment:
import androidx.navigation.fragment.findNavController
binding.editProfileButton.setOnClickListener {
findNavController().navigate(
R.id.action_profile_to_edit_profile
)
}
The user can now move between three independent flows:
Home → Post Details → Comments
News → Notification Details
Profile → Edit Profile
Switching tabs does not discard the destination previously opened in another tab.
How Does Multiple-Back-Stack Restoration Work?
When a user selects another bottom tab, Navigation UI performs two operations:
- It saves the current tab’s navigation state.
- It restores the previously saved state of the selected tab.
The equivalent manual navigation options would look like this:
import androidx.navigation.NavGraph.Companion.findStartDestination
navController.navigate(selectedDestinationId) {
launchSingleTop = true
restoreState = true
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
}
You do not need this code when using setupWithNavController(). It is shown only to explain the underlying behaviour.
launchSingleTop prevents another copy of the same top-level destination from being added. saveState preserves the stack being left, while restoreState restores the stack associated with the selected destination.
How Is UI State Preserved?
Back-stack restoration and UI-state restoration are related, but they are not identical.
Jetpack Navigation restores the destination and its saved navigation state. Android views with stable IDs can also restore values such as entered text or scroll position when those views support instance-state saving.
Important application data should still be stored in a ViewModel:
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
class HomeViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
var selectedPostId: Long?
get() = savedStateHandle["selected_post_id"]
set(value) {
savedStateHandle["selected_post_id"] = value
}
}
A mistake we frequently see is treating a fragment instance as the application’s source of truth. It may appear to work while switching tabs, but it becomes unreliable after process death or configuration changes.
Use:
ViewModelfor screen and business stateSavedStateHandlefor small values that should survive process recreation- Automatic view state for transient values such as entered text
- A repository or database for persistent application data
Handling Repeated Tab Selections
Some applications scroll the current feed to the top when the selected tab is tapped again. Others return that tab to its root destination.
Use setOnItemReselectedListener() for this behaviour:
binding.bottomNavigation.setOnItemReselectedListener { item ->
if (item.itemId == R.id.home_graph) {
// Scroll the Home feed to the top,
// or return the Home graph to its start destination.
}
}
Avoid calling setupWithNavController() again inside this listener. The bottom navigation is already connected to the controller.
Hiding the Bottom Navigation on Deeper Screens
Instagram-like navigation does not always keep the bottom bar visible on every destination. For example, a full-screen editor or media viewer may need additional space.
Automate Your Android Workflow
We build Android Studio plugins that cut manual effort, improve speed, and make your development process seamless.
You can observe destination changes and control the bottom bar:
import android.view.View
navController.addOnDestinationChangedListener { _, destination, _ ->
val hideBottomNavigation = destination.id in setOf(
R.id.commentsFragment,
R.id.editProfileFragment
)
binding.bottomNavigation.visibility =
if (hideBottomNavigation) View.GONE else View.VISIBLE
}
Whether the bar should remain visible depends on the role of the destination. Keep it visible while users explore a core tab flow, but consider hiding it for immersive or task-focused screens.
Common Problems and Fixes
A tab always opens its first screen
Confirm that the project uses Navigation Component 2.4.0 or later and that the bottom bar is connected through setupWithNavController().
Older Navigation versions do not provide the same automatic multiple-back-stack behaviour.
Selecting a tab does nothing
The bottom-menu item ID must match the nested navigation graph ID:
android:id="@+id/home_graph"
Both the menu and navigation graph must use the exact same ID.
A screen loses entered data
Move meaningful state into a ViewModel or SavedStateHandle. Do not rely only on fragment fields or assume the fragment will remain in memory.
Back opens a screen from another tab
This commonly occurs in manual implementations that place transactions from every tab into one shared fragment back stack. Give each top-level tab its own nested navigation graph.
The same destination opens more than once
Use navigation actions correctly and avoid triggering navigation more than once through rapid button taps. Top-level tab navigation already applies launchSingleTop through Navigation UI.
Fragments are recreated unexpectedly
Fragment recreation is not necessarily an error. Android may recreate fragments during configuration changes or process restoration. Preserve the screen’s state instead of depending on permanent fragment instances.
Should You Use Manual Fragment Transactions?
Manual fragment transactions are still useful when an application requires highly specialized fragment orchestration that the Navigation Component cannot represent.
For standard bottom navigation, however, they usually introduce more code than value. Navigation Component also makes the application flow easier to inspect because destinations and actions are defined in one graph rather than distributed across activity click listeners.
Use manual transactions only when there is a specific behavioural requirement—not simply to prevent fragment recreation.
BottomNavigationView vs TabLayout
| Requirement | BottomNavigationView | TabLayout |
| Top-level application destinations | Best suited | Not recommended |
| Three to five persistent tabs | Yes | Possible |
| Swipeable pages | No | Yes |
| Navigation Component integration | Built in | Requires custom setup |
| Multiple navigation back stacks | Supported | Requires additional handling |
| Category or filter tabs | Possible | Best suited |
| Badges and bottom-navigation styling | Built in | Requires customization |
For an Instagram-style application structure, BottomNavigationView is the appropriate choice.
Frequently Asked Questions
What is Instagram-style bottom navigation?
Instagram-style bottom navigation gives every top-level tab an independent navigation history. Users can switch between tabs and return to the destination and UI state they previously left.
How do you preserve fragment state with bottom navigation?
Create a nested navigation graph for each tab and connect BottomNavigationView using setupWithNavController(). Jetpack Navigation then saves and restores each tab’s navigation stack automatically.
Does BottomNavigationView support multiple back stacks?
Yes. Navigation Component 2.4.0 and later support multiple back stacks when BottomNavigationView is connected to a NavController through the standard Navigation UI integration.
Should I use TabLayout or BottomNavigationView?
Use BottomNavigationView for three to five top-level application destinations. Use TabLayout for closely related pages, categories, filters, or swipeable content displayed through ViewPager2.
Why does my fragment lose its state after switching tabs?
The tab may be recreating its destination, or its data may exist only in fragment fields. Store meaningful screen state in a ViewModel and restoration values in SavedStateHandle.
How does the Back button work with multiple tab stacks?
The NavController moves backwards through the currently selected tab. Because the NavHostFragment is the default navigation host, it integrates with Android’s system and gesture-based Back actions.
Can every bottom-navigation tab contain nested fragments?
Yes. Each tab can use a nested navigation graph containing several fragment destinations. Navigating inside one graph does not discard the saved navigation stack belonging to another tab.
Should the bottom navigation remain visible on every screen?
Not necessarily. Keep it visible across core tab flows, but consider hiding it for immersive destinations, full-screen media, editors, authentication screens, or focused tasks where it could cause accidental navigation.



