December 8th, 2022

Compose TwoPaneLayout updates

Kristen Halper
SW/FW Engineer

Hello Compose developers,

This week, we’d like to announce two major updates to our TwoPaneLayout component that come with version 1.0.1-alpha05. We hope these updates will help make the library even more flexible and useful for you all.

We also have some other Compose SDK updates to announce!

New value in PaneMode enum

It may seem counterintuitive, but we actually decided to add a SinglePane value to the TwoPaneMode enum. As a reminder, the pane mode in TwoPaneLayout helps you choose when you want to show one or two panes. We realized that, in some cases, people may want to change the pane mode dynamically, so we added the SinglePane option to make TwoPaneLayout as flexible as possible.

This table helps explain how TwoPaneLayout will show one (🟩) or two (🟦🟦) panes in different configurations with each pane mode:

Pane mode Small window without separating fold Portrait large window / horizontal separating fold Landscape large window / vertical separating fold
TwoPane 🟩 🟦
🟦
🟦🟦
HorizontalSingle 🟩 🟩 🟦🟦
VerticalSingle 🟩 🟦
🟦
🟩
SinglePane 🟩 🟩 🟩

Nothing has changed regarding how you should use the pane mode, but now with these more flexible options, an example scenario would be something like this:

var paneMode by remember { mutableStateOf(TwoPaneMode.TwoPane) }
val updatePaneMode: (TwoPaneMode) -> Unit = { paneMode = it }

val navController = rememberNavController()

TwoPaneLayoutNav(
    paneMode = paneMode,
    navController = navController,
    singlePaneStartDestination = "A",
    pane1StartDestination = "A",
    pane2StartDestination = "B"
) {
    composable("A") {
        ContentA(
            onClick = { navController.navigateTo("B", Screen.Pane2) }
        )
    }
    composable("B") {
        ContentB(
            onClick = {
                navController.navigateTo("C", Screen.Pane1)
                updatePaneMode(TwoPaneMode.SinglePane)
            }
        )
    }
    composable("C") {
        ContentC(
            onClick = {
                navController.navigateTo("A", Screen.Pane1)
                updatePaneMode(TwoPaneMode.TwoPane)
            }
        )
    }
}

In the code snippet example, you can see how we change the pane mode dynamically depending on the route of the current destination. This way, you can use TwoPaneLayout to support a wide range of app scenarios, instead of having to switch between single and two paned layouts!

Something else you may notice about the code snippet is that TwoPaneLayoutNav has some different syntax when it comes to building the navigation graph. This is the second main update we’d like to discuss!

More complex navigation support

You may remember that, a few months ago, we announced the TwoPaneLayoutNav constructor as a new TwoPaneLayout option for more complex navigation scenarios.

The first iteration provided a way to have more than two destinations, and to navigate successfully between all of the provided destinations.

However, we felt that some improvements could be made in a few areas:

  1. Simplifying syntax
  2. Maintaining backstack state
  3. Supporting back press

With version 1.0.1-alpha05 of TwoPaneLayout, we’re happy to say that all of these issues have been addressed!

Simplifying syntax

The first versions of TwoPaneLayoutNav required you to pass in a list of Destination objects, which were a custom class we created to hold the route/composable content.

To simplify the syntax and make it more similar to how you would use a NavHost, we’ve removed the destinations parameter and replaced it with the builder parameter, which accepts NavGraphBuilder DSL.

Here’s an example of how you can migrate your app to the newest TwoPaneLayout version:

Previous syntax:

TwoPaneLayoutNav(
    navController = rememberNavController(),
    singlePaneStartDestination = "A",
    pane1StartDestination = "A",
    pane2StartDestination = "B",
    destinations = arrayOf(
        Destination("A") { ContentA() },
        Destination("B") { ContentB() },
        Destination("C") { ContentC() }
    )
)

New syntax:

TwoPaneLayoutNav(
    navController = rememberNavController(),
    singlePaneStartDestination = "A",
    pane1StartDestination = "A",
    pane2StartDestination = "B"
) {
    composable("A") {
        ContentA()
    }
    composable("B") {
        ContentB()
    }
    composable("C") {
        ContentC()
    }
}

As you can see, this syntax matches Compose formatting better, and it’s more intuitive for developers to use since it’s so similar to NavHost syntax. The key to making this change was overloading the composable function to add TwoPaneNavScope as a receiver to any composable content added to the graph. Make sure you’re using this overloaded version of the function, instead of the original version, because then you can still access TwoPaneNavScope fields and functions! The correct version to import is:

import com.microsoft.device.dualscreen.twopanelayout.twopanelayoutnav.composable

Maintaining backstack state

Another enhancement we made was adding backstack support. Previously, TwoPaneLayoutNav had backstack support in single screen mode provided by the NavHost component, but once an app was spanned, none of this backstack data was saved or used.

Now, we keep track of an internal backstack that will save state when switching between one and two panes. The navigation methods provided by TwoPaneNavScope, navigateTo and navigateBack, will make sure that the backstack is updated correctly, but you also have access to the backStack in the form of a mutable list through the twoPaneBackStack field.

This animation shows an example of how the new TwoPaneLayoutNav will keep track of which destinations you’re in and which destinations you’ve been to:

Animation of the TwoPane navigation stack when running on one or two screens on Surface Duo

Supporting back press

Finally, we’ve added better back press support. Previously, since we didn’t maintain a backstack, you would either have to call navigateUp in single pane mode or call navigateTo in two pane mode to get to any previous destinations.

Now, we have the navigateBack method, which pops the most recent destination from the backstack or finishes the activity if only 1 or 2 backstack entries remain.

By default, back gesture handling is only implemented in single pane mode, but you can add support in two pane mode with BackHandler. Just make sure you call navigateBack so the backstack is updated correctly!

Other Compose SDK updates

Besides these TwoPaneLayout changes, we also recently updated our other Compose libraries to have the newest stable dependencies. All of our libraries now depend on, at the minimum, Compose 1.3.0 and have compileSdkVersion 33, as well as gradle version 7.3.0. WindowState has also been updated depend on Jetpack Window Manager 1.1.0-alpha04, so we were able to delete our custom WindowSizeClass implementation and use the definition from the window-core library.

Feedback and resources

If you have any questions or would like to tell us about your Jetpack Compose applications, use the feedback forum or message us on Twitter @surfaceduodev.  

Finally, please join us every Friday on Twitch at 11am Pacific Time to chat about Surface Duo developer topics!

Author

Kristen Halper
SW/FW Engineer

Works in the Surface Duo Developer Experience team to help with all aspects of dual-screen SDK development and customer engagement.

0 comments

Discussion are closed.