Tabbed SlidingPaneLayout (Primary/Detail) using the Navigation Component Library ➡️🗔✨

Daniel Wilson
4 min readMar 2, 2022

This is a 2022 update to a previous, flawed attempt using the Jetpack Navigation Component library to add tabs to a Primary/Detail layout. I would like to thank Mark Allison’s article on the SlidingPaneLayout for this article, it was a very useful resource so please check it out. The code for this article is here.

A lot has changed, N.B. this solution does not use Jetpack Compose.

Basic primary/detail SlidingPaneLayout view wrapped in a ViewPager2
New Project Dialog Samples
  • Primary/Detail navigation (we called it Master/Detail back then 😏) is a bit cooler now. Foldables and responsive layouts are more popular, the New Project dialog has a Responsive Activity and the Primary/Detail code has been rewritten to use the Navigation component.
  • The Navigation component docs have been significantly expanded.
  • A new Two-pane layout doc was added, and the SlidingPaneLayout ViewGroup has been updated.
  • ViewPager was deprecated for ViewPager2

My previous solution had a locked orientation 🤔. It wrapped a primary or primary/detail fragment in a ViewPager and used an is_tablet flag to determine which fragment to display. On sub sw600dp devices (phones), the detail view was even in it’s own Activity for an easier backstack management.

The main issue with this approach is that it was too complex if we need to support foldables or rotation. is_tablet is not smart enough when the orientation can change, and having an activity for the detail view on portrait devices but not on landscape ones is a state management nightmare.

Sliding pane what?

The SlidingPaneLayout is actually very old and underused. In 8 years I had never heard of it before the new two-pane doc was added. It’s simple, clever and very suitable for this use case. It behaves like a LinearLayout with brains.

Instead of manually returning a primary/detail fragment or a primary fragment based on screen size, this thing figures out what to show by itself based on the screen and pane widths.

In this case if list_width + item_width is ≤ the screen width it shows both, and the layout_weight supersedes the item_width, causing it to occupy the screen remainder.

Otherwise it shows one container at a time. Navigating to detail in that case simply requires calling open on the SlidingPaneLayout.

There’s no portrait-only-activity hacks and all the code is shared, so there’s a lot less! But fundamentally it encourages you to not think about the layouts based on some arbitrary screen rules. If Samsung produce a phone accordion in a few years it doesn’t matter. Do checkout the Styling Android article for a better description

Tabs 📑

The tricky bit (which I have not seen before and is the reason for this article), is wrapping the SlidingPaneLayout in a ViewPager2.

To support tabs we need to kill the SlidingPaneLayout’s sliding behavior otherwise the user is going to get very confused swiping tabs which also close detail views.

Navigation 🚢

Nav graphs for either pane are set dynamically as we need to pass arguments to them. In the sample code I am just sending the tab number so we know which tab we are on. The detail pane gets a string selection when an item is clicked in the primary fragment.

I also wanted to stick to the Safe Args gradle plugin. So the details_nav_graph has a global action to launch the detail view.

When a selection is observed from a shared ViewModel, regardless of screen layout the fragment is opened. Calling open() runs a sliding animation, and does nothing on screens which can show both panes.

Back button 🔙

Finally we need to tailor the back behavior. The docs explain we need custom back behavior to close() the SlidingPaneLayout when looking at a single pane detail. However with tabs we need some extra behavior to restore the custom back handling to support sliding between tabs.

If you open a detail view on tab 1, swipe right and open one on tab 2, then swipe left back to tab 1, the back button needs to close() the SlidingPaneLayout instead of closing the app. To achieve that, in each tab’s onResume() and onPause() I reset the isEnabled flag on the OnBackPressed() callback.

Two extra pieces of fun are that an extra onLayout() needs to be requested to reset an internal SlidingPaneLayout flag, otherwise close() will do nothing with we swap tabs. And we can’t set isEnabled until after the view has been laid out in the case of orientation changes.

You can see in the final result the selection is retained across orientation changes and the back button behavior is dependent on a combination of one vs two panes, and whether or not we are looking at an open detail pane.

Once again please checkout the full code, thanks for reading! 🧙

--

--