{"id":2726,"date":"2022-09-01T11:30:16","date_gmt":"2022-09-01T18:30:16","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/surface-duo\/?p=2726"},"modified":"2022-09-08T09:49:25","modified_gmt":"2022-09-08T16:49:25","slug":"jetpack-compose-existing-app","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/surface-duo\/jetpack-compose-existing-app\/","title":{"rendered":"Dual-screen example adds Jetpack Compose to the experience"},"content":{"rendered":"<p>\n  Hello Compose developers!\n<\/p>\n<p>\n  This week, we\u2019re excited to announce two big updates to our dual-screen experience example: a <a href=\"https:\/\/github.com\/microsoft\/surface-duo-dual-screen-experience-example\/commit\/b27b34168ddb0fc1ae0e066da7efebc7b0a93167\">Compose refactor of the catalog page<\/a> and a <a href=\"https:\/\/github.com\/microsoft\/surface-duo-dual-screen-experience-example\/commit\/76cf7dff0551dc363f82b51a593f27337f53b9bd\">brand new order history page<\/a>.\n<\/p>\n<p>\n  The dual-screen experience example provides an end-to-end experience centered around a travelling guitar salesperson, with options to find fake guitar stores, read a catalog, browse products, place fake guitar orders, and now, view order history.\n<\/p>\n<p>\n  <img decoding=\"async\" width=\"1032\" height=\"814\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-1.png\" class=\"wp-image-2727\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-1.png 1032w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-1-300x237.png 300w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-1-1024x808.png 1024w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-1-768x606.png 768w\" sizes=\"(max-width: 1032px) 100vw, 1032px\" \/><br\/><em>Figure 1. Screenshot of the new catalog page<\/em>\n<\/p>\n<p>\n  The main goal of this sample is to highlight the different ways developers can take advantage of the dual-screen\/foldable form factor. With these two new Compose additions, we hope that this sample can continue to serve as a great example for an even larger audience of developers. The <a href=\"https:\/\/play.google.com\/store\/apps\/details?id=com.microsoft.device.samples.dualscreenexperience\">new version<\/a> of the app is now available in the Google Play Store!\n<\/p>\n<h2>Catalog page refactor<\/h2>\n<p>\n  Before the code refactor, we used fragments for each page.\n<\/p>\n<p>\n  Now, every page is wrapped with the <code>PageLayout<\/code> composable function to hold the content of the page, so we don\u2019t have to set up the bottom page number or the constraints every time.\n<\/p>\n<pre>@Composable\r\nfun PageLayout(\r\n    modifier: Modifier = Modifier,\r\n    pageNumber: Int,\r\n    maxPageNumber: Int,\r\n    content: @Composable () -&gt; Unit\r\n) {\r\n    val constraintSet = getMainConstraintSet()\r\n\r\n    ConstraintLayout(constraintSet = constraintSet, modifier = modifier) {\r\n        Box(\r\n            modifier = Modifier\r\n                .layoutId(CONTENT_ID)\r\n                .fillMaxHeight()\r\n                .padding(bottom = dimensionResource(id = R.dimen.catalog_margin_normal))\r\n        ) {\r\n            content()\r\n        }\r\n        BottomPageNumber(\r\n            modifier = Modifier.layoutId(BOTTOM_PAGE_NUMBER_ID),\r\n            text = stringResource(\r\n                id = R.string.catalog_page_no, pageNumber, maxPageNumber\r\n            )\r\n        )\r\n    }\r\n}<\/pre>\n<p>\n  Within <code>PageLayout<\/code> composable, and throughout most of the refactor, we decided to use the <code>ConstraintLayout<\/code> composable function to create our UI screens.\n<\/p>\n<h3>What is ConstraintLayout\u00a0?<\/h3>\n<p>\n  <code>ConstraintLayout<\/code>\u00a0is a layout that is mostly used when building complex layouts. According to the <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/layouts\/constraintlayout\">official Android documentation<\/a>, it \u201callows you to place composables relative to other composables on the screen.\u201d\n<\/p>\n<h3>When should I use ConstraintLayout in Jetpack Compose?<\/h3>\n<p>\n  The documentation recommends using ConstraintLayout when you want to:\n<\/p>\n<p>\n  &#8211; avoid nesting multiple Columns and Rows\n<\/p>\n<p>\n  &#8211; position composable relative to other composables with the help of guidelines, barriers, or chains\n<\/p>\n<h3>Using constraints to place composables<\/h3>\n<p>\n  If you have a foldable or a dual-screen device, you can create a vertical barrier and place composables relative to it.\n<\/p>\n<p>\n  For example, let\u2019s take the <a href=\"https:\/\/github.com\/microsoft\/surface-duo-dual-screen-experience-example\/blob\/main\/app\/src\/main\/java\/com\/microsoft\/device\/samples\/dualscreenexperience\/presentation\/catalog\/ui\/FifthPageContent.kt\">fifth page of the catalog<\/a> from our sample.\n<\/p>\n<p>\n  This is the layout in dual-landscape orientation: \n<\/p>\n<p>\n  <img decoding=\"async\" width=\"617\" height=\"788\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-2.png\" class=\"wp-image-2728\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-2.png 617w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-2-235x300.png 235w\" sizes=\"(max-width: 617px) 100vw, 617px\" \/>\n<\/p>\n<p><em>Figure 2. Fifth page of the catalog when viewed in the dual-landscape orientation<\/em>\n<\/p>\n<p>\n  And this is the layout for dual-portrait orientation:\n<\/p>\n<p>\n  <img decoding=\"async\" width=\"774\" height=\"611\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-3.png\" class=\"wp-image-2729\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-3.png 774w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-3-300x237.png 300w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-3-768x606.png 768w\" sizes=\"(max-width: 774px) 100vw, 774px\" \/><br\/><em>Figure 3. Fifth page of the catalog when viewed in the dual portrait orientation<\/em>\n<\/p>\n<p>\n  To create this UI, we used a <code>ConstraintSet<\/code> to place TextDescription and RoundedImage, which are just some styled Text and Image composable functions. For each item, we need to set a layout ID in order to create a reference to the item and position it in the layout.\n<\/p>\n<pre>modifier = Modifier.layoutId(FIFTH_PAGE_FIRST_TEXT_ID)<\/pre>\n<p>\n  In the first step, we are creating the reference for each TextDescription and RoundedImage.\n<\/p>\n<pre>val firstTextRef = createRefFor(FIFTH_PAGE_FIRST_TEXT_ID)\r\nval firstImageRef = createRefFor(FIFTH_PAGE_FIRST_IMAGE_ID)\r\nval secondImageRef = createRefFor(FIFTH_PAGE_SECOND_IMAGE_ID)\r\nval secondTextRef = createRefFor(FIFTH_PAGE_SECOND_TEXT_ID)<\/pre>\n<p>\n  In the second step, we are creating the guidelines in order to help us place the composables depending on the layout orientation.\n<\/p>\n<pre>val verticalGuideline = createGuidelineFromStart(0.5f)\r\nval horizontalGuideline = createGuidelineFromTop(0.5f)<\/pre>\n<p>\n  Let\u2019s constrain the reference of the first TextDescription. The start position will be linked to the start position of the parent, also for the top position but with a margin.\n<\/p>\n<pre>constrain(firstTextRef) {\r\n     start.linkTo(parent.start)\r\n     top.linkTo(parent.top, topMargin)\r\n}<\/pre>\n<p>\n  All good so far, but what if we need to constrain the composable depending on the orientation? Let\u2019s take a close look at the first image reference. We are using the boolean variable <code>isFeatureHorizontal<\/code> to determine the orientation.\n<\/p>\n<p>\n  <img decoding=\"async\" width=\"614\" height=\"784\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-4.png\" class=\"wp-image-2730\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-4.png 614w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-4-235x300.png 235w\" sizes=\"(max-width: 614px) 100vw, 614px\" \/>\n<\/p>\n<p><em>Figure 4. Fifth page of the catalog with vertical and horizontal guidelines highlighted<\/em>\n<\/p>\n<p>\n  This is a visual representation of each reference. For better understanding, we have drawn one red line which represents the horizontalGuideline, and one green line which represents the verticalGuideline. Both are invisible, but in our case, we made them visible to see what happens behind the scenes.\n<\/p>\n<pre>constrain(firstImageRef) {\r\n    start.linkTo(parent.start)\r\n    end.linkTo(verticalGuideline)\r\n    if (isFeatureHorizontal) {\r\n        bottom.linkTo(horizontalGuideline)\r\n        top.linkTo(firstTextRef.bottom)\r\n  } else {\r\n        top.linkTo(firstTextRef.bottom, topMargin)\r\n    }\r\n    height = Dimension.preferredWrapContent\r\n    width = Dimension.preferredWrapContent\r\n}<\/pre>\n<p>\n  The start of the first image will be linked to the start position of the parent and the end position will be linked to the vertical guideline. This way we make sure that the image is in the center between the start of the screen and the vertical guideline.\n<\/p>\n<p>\n  If the folding feature is horizontal, the bottom position of the image will be linked to the horizontal guideline and the top position will be linked to the bottom position of the first text. If the folding feature is not horizontal, then we link only the top position to the bottom of the first text with a margin.\n<\/p>\n<p>\n  After we are done setting up all constraints for the composables, we can pass the ConstraintSet to the ConstraintLayout.\n<\/p>\n<pre>ConstraintLayout(\r\n    constraintSet = constraintSet,\r\n    modifier = modifier\r\n) { }<\/pre>\n<h2>New order history page<\/h2>\n<p>\n  The order history page is a new fifth destination in the app\u2019s bottom navigation bar. In this tab, users can view details from their previous orders, such as date and price, and they can also choose to re-order customized items.\n<\/p>\n<p>\n  There are three main features of this new page:\n<\/p>\n<ul>\n<li>\n    Updated first-run experience (FRE)\n  <\/li>\n<li>\n    List detail app pattern\n  <\/li>\n<li>\n    Add to order dialog\n  <\/li>\n<\/ul>\n<h3>Update first-run experience<\/h3>\n<p>\n  The tutorial has an updated step after a user has completed their first order. Instead of directing users back to the store&#8217;s product page, it now encourages users to check out the order history page to view the receipt from their first order.\n<\/p>\n<p>\n  <img decoding=\"async\" width=\"618\" height=\"943\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-5.png\" class=\"wp-image-2731\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-5.png 618w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-5-197x300.png 197w\" sizes=\"(max-width: 618px) 100vw, 618px\" \/><br\/><em>Figure 5. Screenshot of the new step in the app FRE.<\/em>\n<\/p>\n<h3>List detail app pattern<\/h3>\n<p>\n  Like the products page, the order history page uses the <a href=\"https:\/\/docs.microsoft.com\/dual-screen\/design\/list-detail\">list detail app pattern<\/a> when shown on dual-screen and foldable devices.\n<\/p>\n<p>\n  In the first pane, a list of previous orders is displayed. The orders are represented by a card that contains a preview of which guitars were in the order, when the order was placed, and how much it cost.\n<\/p>\n<p>\n  <img decoding=\"async\" width=\"1032\" height=\"814\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-6.png\" class=\"wp-image-2732\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-6.png 1032w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-6-300x237.png 300w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-6-1024x808.png 1024w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-6-768x606.png 768w\" sizes=\"(max-width: 1032px) 100vw, 1032px\" \/><br\/><em>Figure 6.  Screenshot of the order history page in dual-portrait mode, with the list pane on the left and the detail pane on the right.<\/em>\n<\/p>\n<p>\n  In the second pane, a more detailed view of each order can be accessed by clicking on elements from the list pane. The detail view allows users to see more information about items in their order, including the individual order item names, prices, and quantities.\n<\/p>\n<h3>Add to order dialog<\/h3>\n<p>\n  Within the detail pane, users can also click the <strong>View <\/strong>button next to an order item to access the <strong>Add to order<\/strong> dialog. This dialog displays all the details a user would see on the products page, including name, rating, price, and description.\n<\/p>\n<p>\n  <img decoding=\"async\" width=\"1032\" height=\"814\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-7.png\" class=\"wp-image-2733\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-7.png 1032w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-7-300x237.png 300w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-7-1024x808.png 1024w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/08\/word-image-2726-7-768x606.png 768w\" sizes=\"(max-width: 1032px) 100vw, 1032px\" \/><br\/><em>Figure 7. Screenshot of the Add to order dialog.<\/em>\n<\/p>\n<p>\n  It also gives users the opportunity to reorder specific items exactly as they were customized. Let\u2019s say, for instance, a customer really enjoyed a guitar from their previous order, and they now need a replacement. Instead of checking their order history for the exact model, body shape, and color, then navigating back to the products page to input all these customizations, all they have to do is click <strong>Add to order<\/strong> in the dialog to add the exact same product to their cart!\n<\/p>\n<h2>Lessons learned<\/h2>\n<p>\n  We both learned a lot about Compose by making these updates to the dual-screen experience example and we hope this will help you on your Compose learning journey as well! Here are some of our main takeaways from the experience:\n<\/p>\n<p><em>Alin: <\/em>Jetpack Compose is the new way of making UI in Android. The downside of it, as with any new technology, is that you have a learning curve. Coming from the old way of making UI in Android (with XML layouts) can be tough, but here are some lessons and best practices that I encountered:\n<\/p>\n<p>\n  1)\tAvoid reading the official documentation of Compose too much. It\u2019s very good to follow the docs, but you must get your hands dirty and code it yourself.\n<\/p>\n<p>\n  2)\tLet\u2019s say you have a composable function and you want to use different background colors or different padding depending on which screen you\u2019re on. A best practice is that you provide a default modifier to your composable function in order to make changes to your composable very easy.\n<\/p>\n<p>\n  3)\tDon&#8217;t use the modifier passed as a parameter to a composable function more than once. It should only be applied to the top-level layout in your function\n<\/p>\n<p><em>Kristen:<\/em> One of the biggest issues I struggled with was rotating the guitar images correctly! I had to do a lot of research and trial\/error, and you can see in the code that I actually rotated the guitar images in two different ways: 1) using the <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/ui\/graphics\/package-summary#(androidx.compose.ui.Modifier).graphicsLayer(kotlin.Float,kotlin.Float,kotlin.Float,kotlin.Float,kotlin.Float,kotlin.Float,kotlin.Float,kotlin.Float,kotlin.Float,kotlin.Float,androidx.compose.ui.graphics.TransformOrigin,androidx.compose.ui.graphics.Shape,kotlin.Boolean,androidx.compose.ui.graphics.RenderEffect,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color)\">graphicsLayer modifier<\/a>, and 2) using the <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/graphics#canvas\">Canvas composable<\/a>. Depending on how you want to scale an image and align it with other elements, you can choose between these two options. I used the Canvas composable for the <strong>Add to order<\/strong> dialog because it gave me greater control over the size of the composable and how padding was added, but the <code>graphicsLayer<\/code> modifier ending up working better for simpler situations, like in the detail pane.\n<\/p>\n<h2>Resources and feedback<\/h2>\n<p>\n  You can read more about Jetpack Compose for foldable devices in the <a href=\"https:\/\/docs.microsoft.com\/dual-screen\/android\/jetpack\/compose\/\">Surface Duo developer documentation<\/a>. \n<\/p>\n<p>\n  If you have any questions or would like to tell us about your dual-screen applications, use the\u202f<a href=\"http:\/\/aka.ms\/SurfaceDuoSDK-Feedback\" target=\"_blank\" rel=\"noopener\">feedback forum<\/a>\u202for message us on Twitter\u202f<a href=\"https:\/\/twitter.com\/surfaceduodev\" target=\"_blank\" rel=\"noopener\">@surfaceduodev<\/a>.\u202f\u00a0\n<\/p>\n<p>\n  Finally, please join us every Friday on\u202f<a href=\"https:\/\/twitch.tv\/surfaceduodev\" target=\"_blank\" rel=\"noopener\">Twitch<\/a>\u202fat 11am Pacific time to chat about Surface Duo developer topics!\u00a0\u00a0<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hello Compose developers! This week, we\u2019re excited to announce two big updates to our dual-screen experience example: a Compose refactor of the catalog page and a brand new order history page. The dual-screen experience example provides an end-to-end experience centered around a travelling guitar salesperson, with options to find fake guitar stores, read a catalog, [&hellip;]<\/p>\n","protected":false},"author":95422,"featured_media":2732,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[717,692,473,46],"class_list":["post-2726","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-surface-duo-sdk","tag-foldable","tag-jetpack-compose","tag-kotlin","tag-surface-duo"],"acf":[],"blog_post_summary":"<p>Hello Compose developers! This week, we\u2019re excited to announce two big updates to our dual-screen experience example: a Compose refactor of the catalog page and a brand new order history page. The dual-screen experience example provides an end-to-end experience centered around a travelling guitar salesperson, with options to find fake guitar stores, read a catalog, [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts\/2726","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\/95422"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/comments?post=2726"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts\/2726\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/media\/2732"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/media?parent=2726"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/categories?post=2726"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/tags?post=2726"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}