Dual-screen list-detail with navigation

Avatar

Cesar

Hello Kotlin and Java developers!

When you’re enhancing your existing Android apps for the Microsoft Surface Duo, you may want to keep your existing single-screen behavior. In today’s blog, I’ll share an example list-detail that supports the traditional back-button behavior in a single screen, but shows the list and detail side-by-side when the app is spanned across two screens.

Single screen navigation

The expected list-detail user experience on a single screen is for a list view to be replaced by the detail when an item is selected. A back arrow appears in the navigation bar, and the back gesture is supported to return to the list view.

    
Figure 1: Single screen list-detail with back button

Spanned list-detail behavior

When the app is spanned across two screens, the back button isn’t required, but the list should indicate which item is selected.


Figure 2: List-detail spanned across two screens

When the app is unspanned, it should show the single screen view, with the selected detail view showing and the back-stack set to return to the list view.

List-detail navigation with SurfaceDuoLayout

If your app already uses fragments to manage individual views, you can add the SurfaceDuoLayout to your app and implement this navigation by following the sample on GitHub. If your views are still implemented as activities, consider refactoring them into fragments first.

Review these six steps shown in the sample app to enable this navigation behavior:

  1. In the AndroidManifest.xml, choose a theme without an action bar:

    android:theme="@style/Theme.AppCompat.Light.NoActionBar"
  2. In activity_main.xml, add a toolbar and the SurfaceDuoLayout:

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/my_toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:elevation="4dp"
        android:theme="@style/AppTheme"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
    
    <com.microsoft.device.dualscreen.layouts.SurfaceDuoLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    
  3. The sample application does not handle any configuration changes, so in the onCreate method, check whether the app is spanned and set the fragments accordingly. It uses the ScreenHelper class to determine if the app is spanned:

    if (ScreenHelper.isDualMode(this)) {
        supportFragmentManager.beginTransaction()
            .replace(R.id.first_container_id, ListItemsFragment())
            .replace(R.id.second_container_id, DetailFragment(), "detailFragment")
            .commit()
    }
    //when we are in single screen mode and we don't currently show the detail view, 
    //it means we are in the initial state when the app starts
    else if (supportFragmentManager.findFragmentByTag("detailFragment") == null) {
        supportFragmentManager.beginTransaction()
            .replace(R.id.first_container_id, ListItemsFragment())
            .commit()
        supportFragmentManager.popBackStackImmediate()
    } else {
        //when we are back from spanned mode, we want to show the previous detail view
        supportFragmentManager.beginTransaction()
            .replace(R.id.first_container_id, DetailFragment(), "detailFragment")
            .addToBackStack("detailFragmentBackStack")
            .commit()
    }
  4. The list fragment should load and display a list – the example uses a RecyclerView and some hardcoded data. It uses a view model to keep track of the selected item index, and when the app is spanned it disables the back arrow:

    if (!ScreenHelper.isDualMode(requireContext())) {
        (activity as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(false)
    }

    This list fragment also declares an onClickListener for when an item is selected – when the app is on a single screen, the list fragment is replaced with the detail fragment and the back stack is updated:

    if (!ScreenHelper.isDualMode(this)) {
        //We replace this fragment for the detail view when we click on an item and we are in single screen mode
        parentFragmentManager.beginTransaction()
            .replace(R.id.first_container_id, DetailFragment(), "detailFragment")
            .addToBackStack("detailFragmentBackStack")
            .commit()
    }

    When the app is spanned, the view model updates are observed and updated by the details fragment.

  5. The ItemsAdapter used by the list detects when the app is spanned, and highlights the selected item:

    if (ScreenHelper.isDualMode(view.context)) {
        changeItemBackground(position, sharedVM.selectedItemPosition.value as Int, layout)
    }
  6. The detail fragment uses the view model to display the selected item, and detects whether the app is spanned before enabling the back arrow:

    if (!ScreenHelper.isDualMode(requireContext())) {
        (activity as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true)
    }

This approach preserves the expected functionality on single screen devices while providing an enhanced dual-screen experience.

Resources and feedback

You can find additional documentation on the SurfaceDuoLayout, ScreenHelper, and other useful controls in the dual-screen library docs, and browse other Surface Duo code samples.

If you’re a Xamarin developer, you can find an example similar to this post, but using C# and XAML as part of this Microsoft Learn module.

We’d love to hear from you! Please leave us feedback or just share your testing tips using our feedback forum, or message me on Twitter or GitHub.

0 comments

Leave a comment