Tabs for Android

REA recently released major revisions of both our iOS and Android realestate.com.au apps. We made great improvements from both a design and user experience perspective. For the development team these improvements required big changes under the hood.

A key focus of the update for us was moving to a completely new navigation model. Out with the navigation drawer, and in with the tab bar. We reorganized major app features to take a more prominent front and centre standing. This helped introduce new Users, while simultaneously making them more accessible for experienced Users.

As our Android app consisted of numerous Activities tied together using a DrawerLayout, accommodating Tabs meant making some major structural changes. Here I’m going to explain some of the approaches we considered and give an overview of our solution.

Design Requirements

Our new app structure had the following requirements:

  • A tab bar along the bottom of the screen
  • Pushing a new screen of content should not obscure the tab bar
  • Tabs should not lose their state when the user navigates away from them

For Android, our visual requirements follow the new material design Bottom navigation spec. However they stray from the accepted guidelines when considering behaviour from User interactions. Normally when drilling deeper into app content we would launch a new fullscreen Activity, obscuring the tabs. Our requirement was to allow the tabs to remain visible app wide.

Retaining Tab State

Tabs were required to maintain their state during navigation. This meant the app had to provide a back-stack of visited content within each individual Tab.

If you’re unfamiliar with Android development, here’s a brief introduction of the common mechanism used for saving state.

Android apps are commonly constructed from two major components, Activities and Fragments. We use these components to build and render the User Interface. Each are subject to their own complex lifecycle. An important consideration is how the system handles Activities and Fragments once offscreen, where they enter a ’stopped’ state. Once they have been ‘stopped’, the system can destroy them based on memory or resource pressure from any foreground tasks.

This is a problem. When a component is destroyed all the instance state associated with it is lost . In order to retain this state, the system provides a Bundle via the onSaveInstanceState method to save anything important before it’s stopped. If the user navigates back to the destroyed component, the system constructs a new instance and then provides this Bundle so that the state can be reconstructed.

We needed to come up with a solution to store and retrieve the state of Tabs while the user navigates around the app.

ViewPager Solution

Initially we tended toward a solution that relied on the Android SDK’s ViewPager. This component allows the user to swipe through sets of context screens, caching content screens either side of the current content. For this reason, it is often used for image galleries.

ViewPager content screens are defined using Android Fragments, and managing a stack of Fragments is relatively easy using the provided Fragment API. The ViewPager allowed us to manage a set of separate Fragments for the individual tabs by using FragmentPagerAdapter.

Unfortunately, there were a couple of issues with using a ViewPager

  • State loss – Selecting a Tab beyond the immediately adjacent pages causes a new Fragment to be created with no state applied.
  • Memory use – ViewPager keeps adjacent pages in memory.

The state loss problem could be solved by swapping out ViewPager’s standard FragmentPagerAdapter for FragmentStatePagerAdapter. FragmentStatePagerAdapter will store state for all Fragments, not just those immediately adjacent. It will then re-apply this state when the user returns. The behaviour would be correct here – navigating between tabs would not reset the tabs.

Memory use was a larger issue. A ViewPager always retains the Fragments either side of the current page. Considering the size of the app and the potentially deep  back-stack of complex fragments, we decided this wasn’t an acceptable solution.

At this point it was clear that we needed a custom solution. The solution needed to efficiently save and restore state for a back-stack of Fragments, and only for Fragments associated with the current tab.

Our Solution

The state saving and restoring behaviour that FragmentStatePagerAdapter provided was exactly what we needed. We took a look at the source code to get an idea of how we might implement something similar ourselves. The investigation yielded two methods that allowed us to create the behaviour required:

The FragmentManager’s saveFragmentInstanceState method allows us to retrieve the instance state of a given fragment, including the state of its child FragmentManager and its child fragments. We can then re-apply this state by using the fragment’s setInitialSavedState to provide the state before a fragment is added.

Using these methods, the app was restructured to include a single Activity that is responsible for keeping track of which state belongs to which tab. When changing tabs, we grab the current tab instance state, store it, create a fragment for the new tab and (if there’s previous state available) apply it. In the case of backgrounding and foregrounding the app, we use the normal Activity lifecycle to save and restore the tab instance states.

To define the relationship between the Fragments that manage the back-stacks for each tab and the actual content Fragments themselves, we created two interfaces:

public interface Supervisable {
     void setSupervisor(Supervisor supervisor);
 }

public interface Supervisor {
     void push(final Supervisable content);
     void replace(final Supervisable content);
     boolean pop();
 }

SupervisorFragments are responsible for restoring the state of the back-stack as well as applying changes to it. For example, pushing further content on to, or popping content off of the stack. Each tab has a SupervisorFragment. When the main Activity requests it’s state, the SupervisorFragment will return the state of its child FragmentManager, and all of its children. If the tab is then selected again, a new SupervisorFragment will be constructed and the previously saved state will be applied.

SupervisableFragments are the actual content Fragments that we interact with in the app. When they are added to a SupervisorFragment’s back-stack, they are supplied a reference to that Supervisor. Using this reference, the content can decide to push further content to the stack, pop itself off the stack or replace itself.

Issues

If this all sounds interesting and you’re considering a similar structure, there is one gotcha to be aware of. By asking the SupervisorFragments to save state at the time of a tab change, we’re changing the order of some lifecycle methods for Fragments. In the case of a tab change, onSaveInstanceState is being called before onPause . This presents a problem because the user can still interact with the content for a small window after onSaveInstanceState is called. If the interaction results in a FragmentTransaction, an IllegalStateException will be thrown:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

We deal with this issue in our SupervisorFragment by first ensuring that all transactions are complete before saving the state (by using executePendingTransactions). And, secondly, by not committing any transactions that are received after onSaveInstanceState.

Summing Up

It’s always preferable to stick with standard components, but sometimes the behaviours don’t quite fit. In these cases it’s a great opportunity to take the time to find a better solution and learn something about the platform while you’re at it.

We hope you enjoy the new Android app and, if you’re a developer in a similar situation, I hope this post strikes your interest!