A tabbed activity that combines a ViewPager and a TabLayout. Navigation is realized by a host fragment for every tab that can replace the currently hosted fragment with another one and keeps track of a separate back stack.

Separate Back Navigation for a Tabbed View Pager in Android

Nilan Marktanner
8 min readJan 2, 2016

--

When I wanted to create a tabbed view pager with separate back navigation for every tab, I had to dig through various resources and examples to accomplish exactly what I needed. As I had to do quite a bit of adjusting and tinkering to finally get it all working, I decided to publish this tutorial, representing the quintessence of my research.

I’m always grateful for feedback, so please share your thoughts onthis article or the source code itself in a response below. Note that this is the first tutorial I’ve ever written, so any feedback is more than welcome.

I also prepared an Android Studio project featuring a working example showcasing the described behaviour, however I also directly embedded gists in this article so you can follow along the tutorial and copy-paste the gists right into your project structure.

The next section goes more into detail about the feature requirements I had when doing research on this topic. Feel free to skip it and jump right to the one after if you want to start getting your hands dirty. If you’re interested in a bit of background information, just read on.

On Feature Requirements

For a specific use case I encountered, I had the following three major features in mind. First, I needed a tab layout featuring a fixed amount of tabs, where the amount, order and initial view of the tabs is known at development time. Each tab should also provide a separate navigation workflow on its own, meaning that the user should be able to press on UI elements to move to other views inside the current tab while pressing the back button should bring back the most recent view into the tab. Finally, switching to another tab or closing/reopening the app should preserve the state of all tabs. In this case, the state of a tab consists of the current view and the complete navigation history.

A minor required feature was that the tab list should be placed at the bottom of the view as seen in the screenshot above. Note that this goes against Google’s Pure Android design philosophy, but it suited my needs just fine. It is actually trivial to move the tabs back to the top of the view, so don’t worry if you prefer tabs on the top.

Disclaimer: The presented code does not work if your activity supports both landscape and portrait mode — running only in landscape or only in portrait mode works fine though. This is caused by a bug that somehow invalidates the ChildFragmentManager needed to build a navigation history. You can restrict your activity from changing between portrait and landscape mode by adding a line to the activity element in the AndroidManifest.xml of your app.

<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

Just change portrait to landscape if you prefer that. Luckily, my activity was already fixed to portrait mode anyway, so that was no drawback for me. More information on this issue can be found in this stackoverflow thread. If you find a solution for this problem, please let me know as I can update the tutorial then.

There are other features I didn’t implement, as I didn’t need them for my use case, that could be important to you. As I knew beforehand exactly what kind of tabs I needed, I did not include the possibility to add, remove or even reorder tabs dynamically during run time. Furthermore, except for clicking on the tab list and swiping, I didn’t offer ways for tabs to directly interact with each other. If these things are crucial to your situation, your princess is in another castle! Seriously though, if you need one of these features, I’d be happy to discuss your use case in the comments. While it should be possible to add these features to the current solution, there might be other ways that fit your situation better.

Setting up the Tabs

For the basic functionality of tabs, we combine a view pager with a tab layout. Before we start, however, we have to make sure that we meet all required dependencies, so we add them to the app’s build.gradle. Make sure to download the correct SDK version in the SDK Manager or adjust the SDK verison to one that is working for you.

Then we can use all components we need in the layout file for our main activity:

While the view pager allows us to swipe through pages of data, the tab layout is essentially the tab bar providing the ability to switch between tabs by clicking on the according tab label and an indication of the current tab. Note that if you want your tab bar to be at the top of the view, just swap the order of the two elements in activity_main.xml. This has no side effects and the rest of this tutorial stays exactly the same.

To fill the view pager with content, we need to extend the class FragmentPagerAdapter and initialize every tab with the correct fragment in our subclass CustomPagerAdapter. Note that view pagers work with any class that implements the PagerAdapter interface, but for our purposes it’s best to extend FragmentPagerAdapter, as that enables us to directly work with Fragments. As the backstack in Android builds upon the FragmentManager class, that’s exactly what we need later to provide separate back navigation for each tab. Let’s start with creating our main activity though, and adding an entry to our AndroidManifest.xml. The complete manifest looks like this:

And this is the main activity:

Focus on the onCreate() method for now and ignore the other two methods which will be explained in the next section.

In this method, we’re plugging the three elements we have - a view pager, a custom pager adapater and the tab layout - together. Special attention should be paid to the viewPager.setOffscreenPageLimit() method. While setting the limit to 1 is enough for our example, you have to adjust this number if you have more than 2 tabs to ensure the state of every tab is preserved when switching tabs. If this limit is too low, tabs far from the current tab will be cleared from memory and reinitialized when revisiting, breaking my major requirements that all tab states are preserved upon tab switching or when closing the app. Note that 1 is the default value for the limit, but I still included the call to stress the importance of this method. Our custom pager adapter looks like this:

In initializeTabs(), we add two instances of the ContentFragments class to our tab list. We use them as a mock of fragments with more elaborate functionality. A ContentFragment inflates this layout:

The ContentFragment does nothing spectacular, as it only has mock functionality:

The ContentFragment class has a depth and a font size that is used to print out its current depth. The depth helps us to verify that the separate backstacks work and simulate navigating between views by setting the depth to the position of the content fragment in the current tab’s backstack. If you’re not familiar with backstacks for fragments you should read this.

Of course you can (and most probably should) initialize your tab list with different kinds of fragments.

Separate Back Navigation for each Tab

If you’ve carefully read the source code of the CustomPagerAdapter class, you probably noticed that we’re not just throwing content fragments inside the tab list - we’re wrapping each of them in a host fragment first. Let’s have a look on the HostFragment class:

The HostFragment class main work happens in the replaceFragment() method. Calling it replaces the content of host fragment with another fragment of any type and adds the formerly hosted fragment to the backstack of the host fragment. Notice that we make use of the child fragment manager of the host fragment. This is crucial, as using the support fragment manager here would mess with the main activity’s backstack, resulting in weird behaviour.

HostFragment extends the abstract class BackStackFragment, which already appeared in our main activity.

The static method handleBackPressed() is called in the main activity when the user presses the back button. We’re iterating over every fragment inside the provided fragment manager - in our setup those are the host fragments. If we find a fragment that is visible and is of type BackStackFragment we found the fragment that should receive the back button press event and let the fragment handle it by calling this fragment’s onBackPressed() method where we are essentially repeating this procedure recursively. If we find an appropriate fragment, we’re popping it from the backstack and signal to the main activity that it does not need to handle the back button press anymore. Otherwise we tell the main activity that we found no appropriate fragment and handles the back button press itself.

Finally, when inflating the layout in the ContentFragment’s onCreateView() method, we’re setting up the floating action button with the openNewContentFragment() method from the main activity to actually push fragments onto the stack, which equals letting the user navigate deeper into the fragment hierarchy.

Now that I covered every part of the solution I encourage you to try out the app yourself. You can find the example project over at Github. Again, I welcome you to share your questions or thoughts on this tutorial and leave a response below.

Conclusion

We built a basic tabbed view by using a view pager in combination with a tab layout. To provide a separate back navigation for every tab, instead of directly filling the tabs with the final fragments, we first embedded them inside a host fragment, that keeps track of the navigation and is responsible for back and forward navigation. Each host fragment uses its child fragment manager for this purpose. To prevent the main activity to “steal” the back button presses away from our current host fragment, we forward the event to the fragment we deem appropriate.

Further Resources and References

The following resources might be interesting to you to enhance upon the presented solution.

  • A guide from codepath about styling the tabs of your tab layout. This includes information in changing the tab style in general and also explains how to use images as tab labels amongst other things.
  • Another guide from codepath about view pagers and fragment pager adapters. If you use a large amount of tabs or often need deep navigation histories, you should especially check out the section about SmartFragmentPagerAdapter. You can simply let the CustomPagerAdapter extend the SmartFragmentPagerAdapter class, nothing else has to be adjusted.

These resources were used to build my solution.

--

--