{"id":2556,"date":"2022-06-23T16:36:09","date_gmt":"2022-06-23T23:36:09","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/surface-duo\/?p=2556"},"modified":"2022-06-23T16:36:09","modified_gmt":"2022-06-23T23:36:09","slug":"jetpack-compose-foldable-twopanelayout","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/surface-duo\/jetpack-compose-foldable-twopanelayout\/","title":{"rendered":"Jetpack Compose TwoPaneLayout update"},"content":{"rendered":"<p>\n  Hello Jetpack Compose developers!\n<\/p>\n<p>\n  This week, we\u2019re excited to announce some big updates to <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/tree\/main\/TwoPaneLayout\">TwoPaneLayout<\/a>, our Jetpack Compose component for foldables and large screens.\n<\/p>\n<p>\n  We\u2019ve just added a new TwoPaneLayout constructor with highly customizable navigation support, and the TwoPaneScope interface has been upgraded to provide more information while also protecting access to its fields and methods. These changes were substantial enough that we decided to bump to TwoPaneLayout version 1.0.1-xx, so now TwoPaneLayout 1.0.0 is the last version that still uses the old API.\n<\/p>\n<p>\n  To see examples of how to use and migrate to the newest TwoPaneLayout version, check out the <a href=\"https:\/\/docs.microsoft.com\/dual-screen\/android\/jetpack\/compose\/two-pane-layout\">documentation<\/a>, library samples (<a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/tree\/main\/TwoPaneLayout\/sample\">TwoPaneLayout<\/a> and <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/tree\/main\/TwoPaneLayout\/nav_sample\">TwoPaneLayoutNav<\/a>), and the <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-samples\/\">Surface Duo Compose samples<\/a>.\n<\/p>\n<p>\n  <img decoding=\"async\" width=\"909\" height=\"720\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/surface-duo-2-showing-navigation-sample-with-pane.png\" class=\"wp-image-2557\" alt=\"Surface Duo 2 showing navigation sample with pane 1 and pane 2 screens visible\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/surface-duo-2-showing-navigation-sample-with-pane.png 909w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/surface-duo-2-showing-navigation-sample-with-pane-300x238.png 300w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/surface-duo-2-showing-navigation-sample-with-pane-768x608.png 768w\" sizes=\"(max-width: 909px) 100vw, 909px\" \/>\n<\/p>\n<h2>New TwoPaneLayoutNav constructor<\/h2>\n<p>\n  TwoPaneLayout now offers the TwoPaneLayoutNav constructor, which is meant to be used in scenarios with complex navigation. We originally started looking into this idea after <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/issues\/7#issuecomment-1004611040\">receiving some feedback on GitHub<\/a>, and we eventually decided that a new constructor would be the best solution:\n<\/p>\n<pre>@Composable\r\nfun TwoPaneLayoutNav(\r\n    modifier: Modifier = Modifier,\r\n    navController: NavHostController,\r\n    paneMode: TwoPaneMode = TwoPaneMode.TwoPane,\r\n    destinations: Array&lt;Destination&gt;,\r\n    singlePaneStartDestination: String,\r\n    pane1StartDestination: String,\r\n    pane2StartDestination: String\r\n)<\/pre>\n<p>\n  As opposed to the original TwoPaneLayout constructor, TwoPaneLayoutNav can be used for apps that require more than two screens of content. Instead of passing in fixed composables for panes 1 and 2, you can now pass in an array of multiple app destinations via the <code>destinations<\/code> parameter. To control which destination is shown in each pane, you can then use internal TwoPaneLayoutNav navigation methods.\n<\/p>\n<p>\n  If we look inside the implementation of TwoPaneLayoutNav, we can see that the code is very similar to TwoPaneLayout when only one pane is shown. We still use a <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/blob\/main\/TwoPaneLayout\/library\/src\/main\/java\/com\/microsoft\/device\/dualscreen\/twopanelayout\/twopanelayoutnav\/TwoPaneLayoutNavCore.kt#L56\">NavHost in the SinglePaneContainer<\/a>, but now it contains multiple destinations instead of only two.\n<\/p>\n<pre>@Composable\r\ninternal fun SinglePaneContainer(\r\n    navController: NavHostController,\r\n    pane1: @Composable TwoPaneScope.() -&gt; Unit,\r\n    pane2: @Composable TwoPaneScope.() -&gt; Unit\r\n) {\r\n    ...\r\n    NavHost(\r\n        navController = navController,\r\n        startDestination = currentSinglePane\r\n    ) {\r\n        composable(Screen.Pane1.route) {\r\n          TwoPaneScopeInstance.pane1()\r\n        }\r\n        composable(Screen.Pane2.route) {\r\n          TwoPaneScopeInstance.pane2()\r\n        }\r\n    }\r\n}<\/pre>\n<pre>\r\n@Composable\r\ninternal fun SinglePaneContainer(\r\n    destinations: Array&lt;Destination&gt;,\r\n    startDestination: String,\r\n    navController: NavHostController,\r\n) {\r\n...\r\n  NavHost(\r\n        navController = navController,\r\n        startDestination = startDestination\r\n    ) {\r\n        destinations.forEach { pane -&gt;\r\n          composable(pane.route) {\r\n              TwoPaneNavScopeInstance.(pane.content)()\r\n            }\r\n      }\r\n  }\r\n}<\/pre>\n<p>\n  The main difference in implementation is within the TwoPaneContainer, because now we have to keep track of the routes for the current pane 1 and 2 destinations. These routes are used to <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/blob\/main\/TwoPaneLayout\/library\/src\/main\/java\/com\/microsoft\/device\/dualscreen\/twopanelayout\/twopanelayoutnav\/TwoPaneLayoutNavCore.kt#L105\">access the content that will be shown in each pane of the layout<\/a>.\n<\/p>\n<pre>@Composable\r\ninternal fun TwoPaneContainer(\r\n    windowState: WindowState,\r\n    modifier: Modifier,\r\n    pane1: @Composable TwoPaneScope.() -&gt; Unit,\r\n    pane2: @Composable TwoPaneScope.() -&gt; Unit\r\n) {\r\n    ...    \r\n    Layout(\r\n        content = {\r\n          TwoPaneScopeInstance.pane1()\r\n              TwoPaneScopeInstance.pane2()\r\n          },\r\n          measurePolicy = measurePolicy,\r\n          modifier = modifier\r\n    )\r\n}<\/pre>\n<pre>@Composable\r\ninternal fun TwoPaneContainer(\r\n    windowState: WindowState,\r\n    modifier: Modifier,\r\n    destinations: Array&lt;Destination&gt;,\r\n    pane1StartDestination: String,\r\n    pane2StartDestination: String\r\n) {\r\n  ...\r\n  var currentPane1 by rememberSaveable { mutableStateOf(pane1StartDestination) }\r\n  var currentPane2 by rememberSaveable { mutableStateOf(pane2StartDestination) }\r\n  \/\/ Find the destinations to display in each pane\r\n  val pane1 = findDestination(currentPane1, destinations).content\r\n  val pane2 = findDestination(currentPane2, destinations).content\r\n  Layout(\r\n        content = {\r\n          TwoPaneNavScopeInstance.pane1()\r\n          TwoPaneNavScopeInstance.pane2()\r\n        },\r\n        measurePolicy = measurePolicy,\r\n        modifier = modifier\r\n    )\r\n}<\/pre>\n<p>\n  To see an example of TwoPaneLayoutNav in action, you can check out our updated <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-samples\/tree\/main\/NavigationRail\">NavigationRail sample<\/a>. If you read the <a href=\"https:\/\/devblogs.microsoft.com\/surface-duo\/jetpack-compose-navigation-rail\/#navigation-hierarchy\">blog post announcing the release of the sample<\/a>, you may remember how complicated the original navigation hierarchy was; we had to use a combination of internal TwoPaneLayout navigation and our own NavHost\/navController combo to achieve the desired app behavior. Now, after switching to TwoPaneLayoutNav, the navigation setup for the app is much simpler because we can achieve all of the same navigation patterns with just one shared NavHost\/navController pair.\n<\/p>\n<h2>Updated scopes and testing support<\/h2>\n<p>\n  The new TwoPaneLayout version also includes updates to TwoPaneScope and the addition of TwoPaneNavScope. In addition, we\u2019ve added new test scope instances to help with handling the API changes in UI tests.\n<\/p>\n<h3>TwoPaneScope<\/h3>\n<p>\n  Previously, TwoPaneScope was only used to provide access to the <a href=\"https:\/\/docs.microsoft.com\/dual-screen\/android\/jetpack\/compose\/two-pane-layout#weight\">weight modifier attribute<\/a>, while the <code>navigateToPane1<\/code> and <code>navigateToPane2<\/code> methods were accessible anywhere. Now, to ensure that certain methods and fields can only be accessed from within the proper scope, we\u2019ve updated the TwoPaneScope interface to the following:\n<\/p>\n<pre>interface TwoPaneScope {\r\n    fun Modifier.weight(weight: Float): Modifier\r\n    fun navigateToPane1()\r\n    fun navigateToPane2()\r\n    val currentSinglePaneDestination: String\r\n    val isSinglePane: Boolean\r\n}<\/pre>\n<p>\n  So for instance, let\u2019s say your app code looked something like this:\n<\/p>\n<pre>@Composable\r\nfun ExampleApp() {\r\n    TwoPaneLayout(\r\n        pane1 = { Pane1() },\r\n        pane2 = { Pane2() }\r\n  )\r\n}\r\n@Composable\r\nfun Pane1() {\r\n    Text(\r\n        modifier = Modifier.clickable(\r\n            onClick = { navigateToPane2() }\r\n      ),\r\n      text = \"Pane 1!\"\r\n    )\r\n}<\/pre>\n<p>\n  With the new TwoPaneLayout version, <code>navigateToPane1<\/code> and <code>navigateToPane2<\/code> can now only be called within TwoPaneScope, so you would need to update your code to this:\n<\/p>\n<pre>@Composable\r\nfun ExampleApp() {\r\n    TwoPaneLayout<\/em>(\r\n        pane1 = { Pane1() },\r\n        pane2 = { Pane2() }\r\n    )\r\n}\r\n@Composable\r\nfun TwoPaneScope.Pane1() {\r\n    Text(\r\n        modifier = Modifier.clickable(\r\n            onClick = { navigateToPane2() }\r\n        ),\r\n        text = \"Pane 1!\"\r\n    )\r\n}<\/pre>\n<p>\n  Regardless of the API changes, the navigation functionality remains the same, as shown in this animation: \n<\/p>\n<p>\n  <img decoding=\"async\" width=\"1195\" height=\"2500\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/animation-of-twopanelayout-on-a-single-screen-devi-scaled.gif\" class=\"wp-image-2558\" alt=\"Animation of TwoPaneLayout on a single screen device, where navigation between pane 1 and pane 2 is shown\" \/>\n<\/p>\n<p><em>Figure 1. Animation showing <code>navigateToPane1<\/code> and <code>navigateToPane2<\/code> on a single screen device<\/em>\n<\/p>\n<p>\n  There also are additional fields available for use in your apps:\n<\/p>\n<ul>\n<li>\n    The <code>currentSinglePaneDestination<\/code> field reports the route of the current pane by returning either <code>Screen.Pane1.route<\/code> (\u201cpane1\u201d) or <code>Screen.Pane2.route<\/code> (\u201cpane2\u201d). It\u2019s important to note that this value makes sense only when one pane is currently displayed.\n  <\/li>\n<li>\n    The <code>isSinglePane<\/code> field returns true when TwoPaneLayout is only showing one pane, otherwise it returns false. For those of you familiar with our <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/tree\/main\/WindowState#window-mode-properties\">WindowState library<\/a>, this may seem similar to <code>isDualScreen<\/code>, but the difference here is that <code>isSinglePane<\/code> also factors in the selected <code>TwoPaneMode<\/code> for the layout. This is useful in cases where you want to conditionally show content based on the number of panes, such as <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-samples\/blob\/main\/ComposeGallery\/src\/main\/java\/com\/microsoft\/device\/display\/samples\/composegallery\/ui\/view\/DetailPane.kt#L36\">actions in a top bar<\/a>.\n  <\/li>\n<\/ul>\n<h3>TwoPaneNavScope<\/h3>\n<p>\n  To help you use the new TwoPaneLayoutNav constructor, we\u2019ve also created a new interface called TwoPaneNavScope: \n<\/p>\n<pre>interface TwoPaneNavScope {\r\n    fun Modifier.weight(weight: Float): Modifier\r\n    fun NavHostController.navigateTo(\r\n        route: String,\r\n        screen: Screen,\r\n        navOptions: NavOptionsBuilder.() -&gt; Unit = { },\r\n    )\r\n    val currentSinglePaneDestination: String\r\n    val currentPane1Destination: String\r\n    val currentPane2Destination: String\r\n    val isSinglePane: Boolean\r\n}<\/pre>\n<p>\n  Since the TwoPaneLayoutNav constructor doesn\u2019t limit you to only two destinations, <code>navigateToPane1<\/code> and <code>navigateToPane2<\/code> don\u2019t make sense for internal navigation support anymore. Instead, we\u2019ve provided a <code>navigateTo<\/code> method with the following parameters:\n<\/p>\n<ul>\n<li>\n    <code>route<\/code> &#8211; route of the destination you want to navigate to \n  <\/li>\n<li>\n    <code>screen<\/code> &#8211; which screen or pane the destination should be shown in when two panes are displayed (possible values: <code>Screen.Pane1<\/code> or <code>Screen.Pane2<\/code>)\n  <\/li>\n<li>\n    <code>navOptions<\/code> &#8211; optional navigation options that will be used by the navController when one pane is displayed (can be used to implement different patterns, such as <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/blob\/main\/TwoPaneLayout\/nav_sample\/src\/main\/java\/com\/microsoft\/device\/dualscreen\/twopanelayout\/BasicDestination.kt#L36\">circular navigation<\/a>)\n  <\/li>\n<\/ul>\n<p>\n  For example, this animation shows how our <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/tree\/main\/TwoPaneLayout\/nav_sample\">TwoPaneLayoutNav sample<\/a> uses <code>navigateTo<\/code> to set up a navigation pattern that works well regardless of how many panes are displayed:\n<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/compose-twopane-animation.gif\"><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/compose-twopane-animation.gif\" alt=\"Animation of TwoPaneLayout sample on Surface Duo\" width=\"800\" height=\"631\" class=\"alignnone size-full wp-image-2572\" \/><\/a><br\/><em>Figure 2. Animation showing TwoPaneLayoutNav sample behavior in one and two panes.<\/em>\n<\/p>\n<p>\n  Like the updated TwoPaneScope, there are also fields that let you access the number of panes displayed (<code>isSinglePane<\/code>) and which destinations are currently displayed (<code>currentSinglePaneDestination<\/code>,  <code>currentPane1Destination<\/code>, and <code>currentPane2Destination<\/code>).\n<\/p>\n<h3>Test scopes instances<\/h3>\n<p>\n  Due to the changes described above, some composables may now require TwoPaneScope or TwoPaneNavScope to be explicitly specified as receivers. When writing your application code, this is not an issue because TwoPaneLayout provides an internal implementation of the necessary scope. However, when writing UI tests to <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/testing#isolation\">test composables in isolation<\/a>, you may find yourself trying to invoke a composable outside of TwoPaneLayout.\n<\/p>\n<p>\n  For instance, in the TwoPaneLayout sample, the top bar composable requires TwoPaneScope to decide what text to show:\n<\/p>\n<pre>@Composable\r\n  fun TwoPaneScope.TopBar(pane: Int) {\r\n      \/\/ Customize top bar text depending on the pane\r\n      val paneString = if (!isSinglePane) \" \" + stringResource(pane) else \"\"\r\n      TopAppBar(\r\n          title = {\r\n            Text(\r\n                  text = <em>stringResource<\/em>(R.string.<em>app_name<\/em>) + paneString,\r\n                  color = Color.White\r\n              )\r\n          },\r\n          backgroundColor = blue\r\n    )\r\n  }<\/pre>\n<p>\n  When trying to write a UI test just for this composable, though, Android Studio will show you this error:\n<\/p>\n<p>\n  <img decoding=\"async\" width=\"1629\" height=\"583\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/screenshot-of-ui-test-for-top-bar-in-android-studi.png\" class=\"wp-image-2559\" alt=\"Screenshot of UI test for top bar in Android Studio, with the following error: &quot;Unresolved reference. None of the following candidates is applicable because of receiver type mismatch: public fun TwoPaneScope.TopBar(pane: Int): Unit defined in com.microsoft.device.dualscreen.twopanelayout in file MainActivity.kt\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/screenshot-of-ui-test-for-top-bar-in-android-studi.png 1629w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/screenshot-of-ui-test-for-top-bar-in-android-studi-300x107.png 300w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/screenshot-of-ui-test-for-top-bar-in-android-studi-1024x366.png 1024w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/screenshot-of-ui-test-for-top-bar-in-android-studi-768x275.png 768w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/screenshot-of-ui-test-for-top-bar-in-android-studi-1536x550.png 1536w\" sizes=\"(max-width: 1629px) 100vw, 1629px\" \/>\n<\/p>\n<p>\n  To solve this problem, we\u2019ve added empty implementations of both scopes: <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/blob\/main\/TwoPaneLayout\/library\/src\/main\/java\/com\/microsoft\/device\/dualscreen\/twopanelayout\/twopanelayout\/TwoPaneScopeTest.kt\">TwoPaneScopeTest<\/a> and <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/blob\/main\/TwoPaneLayout\/library\/src\/main\/java\/com\/microsoft\/device\/dualscreen\/twopanelayout\/twopanelayoutnav\/TwoPaneNavScopeTest.kt\">TwoPaneNavScopeTest<\/a>. These both allow you to manually set the value of scope fields in the constructor so you can set up your composables with the proper state before running your UI tests.\n<\/p>\n<p>\n  For instance, this is the top bar test updated with TwoPaneScopeTest:\n<\/p>\n<pre>@Test\r\nfun topBar_singlePane_showsCorrectPaneString() {\r\n    composeTestRule.setContent {\r\n        val twoPaneScopeTest = TwoPaneScopeTest(isSinglePane = true)\r\n        twoPaneScopeTest.TopBar(pane = R.string.pane1)\r\n    }\r\n    composeTestRule.onNodeWithText(\r\n          composeTestRule.getString(R.string.app_name) + \" \" + composeTestRule.getString(R.string.pane1)\r\n      ).assertDoesNotExist()\r\n      composeTestRule.onNodeWithText(composeTestRule.getString(R.string.app_name)).assertIsDisplayed()\r\n}<\/pre>\n<h2>Get started with version migration<\/h2>\n<p>\n  By this point in the blog post, I hope you\u2019re convinced that the new TwoPaneLayout version is worth a try \ud83d\ude0a If you were already using TwoPaneLayout before and want to migrate to the newer version, follow these steps:\n<\/p>\n<ol>\n<li><strong>Update version number<\/strong>\n<p>\n  The first and most obvious step is to upgrade your TwoPaneLayout version! The latest version with the new API is 1.0.1-alpha02, so your import statement would be as follows: \n<\/p>\n<pre>implementation \"com.microsoft.device.dualscreen:twopanelayout:1.0.1-alpha02\"<\/pre>\n<\/li>\n<li><strong>Add scope to composables<\/strong>\n<p>\n  The next step is to update any composables that use TwoPaneLayout navigation methods to have TwoPaneScope as a receiver. Since \u201cnavigateToPane1\u201d and \u201cnavigateToPane2\u201d are no longer accessible from anywhere, Android Studio won\u2019t recognize the methods until the proper receiver is added. \n<\/p>\n<\/li>\n<li><strong>Use test scope instances in UI tests<\/strong>\n<p>\n  Once you\u2019ve updated your composables to use TwoPaneScope as a receiver, it\u2019s important to double check existing UI tests in your project. Some may need to be updated to use test scope instances in order to build successfully.\n<\/p>\n<\/li>\n<li><strong>Optional: replace posture checks with isSinglePane<\/strong>\n<p>\n  Instead of choosing layout options based on a combination of device and application logic, you can simplify your project code by switching to the <code>isSinglePane<\/code> field. For instance, if you were previously checking for the dual portrait posture when using the HorizontalSingle pane mode, you can now just check isSinglePane. If your project also uses WindowState, try searching for usages of <code>isDualScreen<\/code>, <code>isDualPortrait<\/code>, or <code>isDualLandscape<\/code> to see if you can simplify your code.\n<\/p>\n<\/li>\n<\/ol>\n<h2>Resources and feedback<\/h2>\n<p>\n  The code for <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/tree\/main\/TwoPaneLayout\/nav_sample\">TwoPaneLayout<\/a> is available on GitHub.\n<\/p>\n<p>\n  If you have any questions, or would like to tell us about your apps, use the <a href=\"http:\/\/aka.ms\/SurfaceDuoSDK-Feedback\">feedback forum<\/a> or message us on <a href=\"https:\/\/twitter.com\/surfaceduodev\">Twitter @surfaceduodev<\/a>.\n<\/p>\n<p>\n  Finally, please join us for our <a href=\"https:\/\/www.twitch.tv\/surfaceduodev\">dual-screen developer livestream<\/a> at 11am (Pacific time) each Friday \u2013 mark it in your calendar and check out the <a href=\"https:\/\/youtube.com\/c\/surfaceduodev\">archives on YouTube<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hello Jetpack Compose developers! This week, we\u2019re excited to announce some big updates to TwoPaneLayout, our Jetpack Compose component for foldables and large screens. We\u2019ve just added a new TwoPaneLayout constructor with highly customizable navigation support, and the TwoPaneScope interface has been upgraded to provide more information while also protecting access to its fields and [&hellip;]<\/p>\n","protected":false},"author":30456,"featured_media":2560,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[692,706,473],"class_list":["post-2556","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-surface-duo-sdk","tag-jetpack-compose","tag-jetpack-window-manager","tag-kotlin"],"acf":[],"blog_post_summary":"<p>Hello Jetpack Compose developers! This week, we\u2019re excited to announce some big updates to TwoPaneLayout, our Jetpack Compose component for foldables and large screens. We\u2019ve just added a new TwoPaneLayout constructor with highly customizable navigation support, and the TwoPaneScope interface has been upgraded to provide more information while also protecting access to its fields and [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts\/2556","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/users\/30456"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/comments?post=2556"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts\/2556\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/media\/2560"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/media?parent=2556"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/categories?post=2556"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/tags?post=2556"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}