{"id":684,"date":"2020-09-01T13:00:04","date_gmt":"2020-09-01T20:00:04","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/surface-duo\/?p=684"},"modified":"2021-05-21T10:26:32","modified_gmt":"2021-05-21T17:26:32","slug":"jetpack-compose-dual-screen-sample","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/surface-duo\/jetpack-compose-dual-screen-sample\/","title":{"rendered":"Jetpack Compose on Microsoft Surface Duo"},"content":{"rendered":"<p>\nHello, Android dual-screen developers!\n<\/p>\n<p>\nToday we are going to talk about how to use the new UI framework, <a href=\"https:\/\/developer.android.com\/jetpack\/compose\">Jetpack Compose<\/a> to build a dual-screen app on the Surface Duo. Jetpack Compose is a new Declarative UI Framework in Android. Instead of using the traditional XML layouts, the developer calls the Composable functions to get the UI elements and modify them. Although <a href=\"https:\/\/android-developers.googleblog.com\/2020\/08\/announcing-jetpack-compose-alpha.html\">Google just released the alpha for Jetpack Compose<\/a>, we still believe it is a good idea to leverage it in the development for dual-screen apps.\n<\/p>\n<p>\n  Here is a <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-samples\/tree\/main\/ComposeSamples\/ComposeGallery\">simple sample<\/a> we built on Surface Duo to demonstrate the use of Jetpack Compose. The sample is using the <a href=\"https:\/\/docs.microsoft.com\/dual-screen\/introduction?WT.mc_id=docs-surfaceduoblog-joyl1216#master-detail\">List-Detail<\/a> app pattern to show a list of image thumbnails on the single screen. After spanning the app into dual-screen mode, the full image will be shown on the other screen. Selecting the image item on the list will update the full image accordingly.\n<\/p>\n<p>\n  <img decoding=\"async\" width=\"1431\" height=\"925\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2020\/09\/a-screenshot-of-a-cell-phone-description-automati.png\" class=\"wp-image-685\" alt=\"Dual-screen Jetpack Compose sample on Surface Duo emulator\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2020\/09\/a-screenshot-of-a-cell-phone-description-automati.png 1431w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2020\/09\/a-screenshot-of-a-cell-phone-description-automati-300x194.png 300w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2020\/09\/a-screenshot-of-a-cell-phone-description-automati-1024x662.png 1024w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2020\/09\/a-screenshot-of-a-cell-phone-description-automati-768x496.png 768w\" sizes=\"(max-width: 1431px) 100vw, 1431px\" \/>\n<\/p>\n<p>\n  <i>Figure 1: Screenshot of the sample built with Jetpack Compose<\/i>\n<\/p>\n<p><strong>Prerequisites<\/strong>\n<\/p>\n<p>Jetpack Compose requires <b>Android Studio 4.2 Canary 8<\/b> which you can download from <a href=\"https:\/\/developer.android.com\/studio\/preview\/\">developer.android.com<\/a>. The following items are also required:<\/p>\n<table>\n<tr>\n<th>\nItem<\/th>\n<th>Version\n<\/th>\n<\/tr>\n<tr>\n<td>\n  Jetpack Compose<\/td>\n<td>1.0.0-alpha01 \n<\/td>\n<\/tr>\n<tr>\n<td>\n  Kotlin<\/td>\n<td>1.4.0\n<\/td>\n<\/tr>\n<tr>\n<td>\n  Gradle<\/td>\n<td>6.6-rc-6\n<\/td>\n<\/tr>\n<tr>\n<td>\n  Android Gradle plugin<\/td>\n<td>4.2.0-alpha08\n<\/td>\n<\/tr>\n<tr>\n<td>\n  AndroidX WindowManager<\/td>\n<td>1.0.0-alpha01\n<\/td>\n<\/tr>\n<\/table>\n<p><\/p>\n<p><strong>Detect dual-screen mode<\/strong>\n<\/p>\n<p>\n  To detect whether or not the app is in dual-screen mode is one of the key points to consider when developing an app on Surface Duo. In the sample, the AndroidX <a href=\"https:\/\/developer.android.com\/reference\/androidx\/window\/WindowManager\">WindowManager<\/a> is used to detect the screen mode. After the view is attached to the window, the <a href=\"https:\/\/developer.android.com\/reference\/androidx\/window\/WindowManager\">WindowManager<\/a> registers a callback for layout changes of the window. \n<\/p>\n<pre>override fun onAttachedToWindow() {\r\n    super.onAttachedToWindow()\r\n    windowManager.registerLayoutChangeCallback(mainThreadExecutor, layoutStateChangeCallback)\r\n}\r\noverride fun onDetachedFromWindow() {\r\n    super.onDetachedFromWindow()\r\n    windowManager.unregisterLayoutChangeCallback(layoutStateChangeCallback)\r\n}<\/pre>\n<p>\nA callback class, <code>LayoutStateChangeCallback<\/code>, is created to handle the <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/window\/WindowLayoutInfo\">WindowLayoutInfo<\/a> as shown below. Using <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/window\/DisplayFeature\">DisplayFeature<\/a> inside <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/window\/WindowLayoutInfo\">WindowLayoutInfo<\/a> to detect that the app is spanned in dual-screen mode. Check out the sample code below:\n<\/p>\n<pre>inner class LayoutStateChangeCallback : Consumer<WindowLayoutInfo> {\r\n    override fun accept(newLayoutInfo: WindowLayoutInfo) {\r\n       \tval isScreenSpanned = newLayoutInfo.displayFeatures.size > 0\r\n       \tappStateViewModel.setIsScreenSpannedLiveData(isScreenSpanned)\r\n    }\r\n}\r\n<\/pre>\n<p>\n  After you have created the callback class, save the screen mode in the <a href=\"https:\/\/developer.android.com\/reference\/androidx\/lifecycle\/ViewModel\">ViewModel<\/a> with <a href=\"https:\/\/developer.android.com\/reference\/androidx\/lifecycle\/LiveData\">LiveData<\/a> to allow the UI to update accordingly.\n<\/p>\n<p><strong>LiveData and State Management<\/strong>\n<\/p>\n<p>\nReacting to state changes is at the very heart of Jetpack Compose. When the composable is subscribed to a state, the function is updated when the value of the state is updated. With <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/runtime\/livedata\/package-summary.html#observeasstate\">observeAsState<\/a>, we can subscribe to the state of <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/runtime\/livedata\/package-summary.html\">LiveData<\/a> in the <a href=\"https:\/\/developer.android.com\/reference\/androidx\/lifecycle\/ViewModel\">ViewModel<\/a> and get the value from the function. <em>Every time there is a new value posted into the <\/em><a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/runtime\/livedata\/package-summary.html\"><em>LiveData<\/em><\/a><em>, the returned <\/em><a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/runtime\/State.html\"><em>State<\/em><\/a><em> is updated, causing recomposition of every <\/em><a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/runtime\/State.html#value:androidx.compose.runtime.State.T\"><em>State.value<\/em><\/a><em> usage.<\/em>\n<\/p>\n<pre>val isScreenSpannedLiveData = appStateViewModel.getIsScreenSpannedLiveData()\r\nval isScreenSpanned = isScreenSpannedLiveData.observeAsState(initial = false).value<\/pre>\n<p><strong>Material Design<\/strong>\n<\/p>\n<p>\n  Jetpack Compose is a combination of <a href=\"https:\/\/developer.android.com\/jetpack\/androidx\/releases\/compose#structure\">7 Maven Group IDs<\/a> within <code>androidx<\/code>:\n<\/p>\n<ul>\n<li>\n    <code>androidx.compose<\/code>\n  <\/li>\n<li>\n    <code>androidx.compose.animation<\/code>\n  <\/li>\n<li>\n    <code>androidx.compose.foundation<\/code>\n  <\/li>\n<li>\n    <code>androidx.compose.material<\/code>\n  <\/li>\n<li>\n    <code>androidx.compose.runtime<\/code>\n  <\/li>\n<li>\n    <code>androidx.compose.ui<\/code>\n  <\/li>\n<li>\n    <code>androidx.ui.test<\/code>\/<code>androidx.ui.tooling<\/code>\n  <\/li>\n<\/ul>\n<p><a href=\"https:\/\/developer.android.com\/jetpack\/androidx\/releases\/compose-material\">androidX.compose.material<\/a> is one of the packages providing Composable functions that reflect the styling principles from the <a href=\"https:\/\/material.io\/design\/material-theming\/implementing-your-theme.html\">Material design specification<\/a>. When creating an empty Compose Activity in Android Studio 4.2, a <strong>Theme.kt<\/strong> file will be created under the <strong>\/ui\/<\/strong> folder. The file is used to set up the app theme with <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/material\/package-summary#materialtheme\">MaterialTheme<\/a>, which will help us support both light mode and dark mode. Inside, there is a function <code>ComposeSampleTheme<\/code> to handle all the material design (<a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/material\/Colors\">colors<\/a>, <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/material\/Typography\">typography<\/a>, <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/material\/Shapes\">shapes<\/a>, content), take the UI content, and apply different app themes based on <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/foundation\/package-summary#issystemindarktheme\">isSystemInDarkTheme<\/a>. The function <code>ComposeSampleTheme<\/code> is typically named after the project. \n<\/p>\n<pre>@Composable\r\nfun ComposeSampleTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {\r\n    val colors = if (darkTheme) {\r\n        DarkColorPalette\r\n    } else {\r\n        LightColorPalette\r\n    }\r\n    MaterialTheme(\r\n        colors = colors,\r\n        typography = typography,\r\n        shapes = shapes,\r\n        content = content\r\n    )\r\n}<\/pre>\n<p><strong>UI layout<\/strong>\n<\/p>\n<p>\n  In Jetpack Compose, all components and functions start with a capitalized letter. Most of them work just like their names. <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/foundation\/package-summary#image_1\">Image<\/a> is creating an ImageView. <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/foundation\/package-summary#text\">Text<\/a> is creating a textView. <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/foundation\/layout\/package-summary.html#spacer\">Spacer<\/a> is putting empty space between the components. <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/material\/package-summary#divider\">Divider<\/a> is drawing a separator line. To display a vertical list, there are several options, <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/foundation\/package-summary#scrollablecolumn\">ScrollableColumn<\/a> and <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/foundation\/lazy\/package-summary#lazycolumnfor\">LazyColumnFor<\/a>\/<a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/foundation\/lazy\/package-summary&quot; \\l &quot;lazycolumnforindexed\">LazyColumnForIndexed<\/a>. They are very easy to use and their implementations are also very similar. The following code is to create a vertical list with an image and two texts in each row. The content comes from the parameter <strong>models<\/strong>:\n<\/p>\n<pre>LazyColumnForIndexed(\r\n    items = models,\r\n    modifier = modifier\r\n) { index, item ->\r\n    Row(\r\n        modifier = Modifier.selectable(\r\n            selected = (index == selectedIndex),\r\n            onClick = {\r\n                appStateViewModel.setImageSelectionLiveData(index)\r\n            }\r\n        ) then Modifier.fillMaxWidth(),\r\n        verticalGravity = Alignment.CenterVertically\r\n    ) {\r\n        Image(asset = imageResource(item.image), modifier = Modifier.preferredHeight(100.dp).preferredWidth(150.dp))\r\n        Spacer(Modifier.preferredWidth(16.dp))\r\n        Column(modifier = Modifier.fillMaxHeight() then Modifier.padding(16.dp)) {\r\n            Text(item.id, modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center), fontSize = 20.sp, fontWeight = FontWeight.Bold)\r\n            Text(item.title, modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center))\r\n        }\r\n    }\r\n    Divider(color = Color.LightGray)\r\n}\r\n<\/pre>\n<p><a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/ui\/Modifier\">Modifier<\/a> is an ordered, immutable collection of elements that decorate or add behavior to composable UI components, such as background, padding, style, and click event. Multiple modifiers can be combined with the keyword <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/ui\/Modifier#then\">then<\/a> (<a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/ui\/Modifier#plus\">+(plus)<\/a> is deprecated) and applied on one component, which needs to accept them as parameters.\n<\/p>\n<pre>Row(\r\n    modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center)\r\n        then Modifier.fillMaxWidth().wrapContentSize(Alignment.Center)\r\n) {\r\n    ShowListColumn(\r\n        models, Modifier.fillMaxHeight().wrapContentSize(Alignment.Center).weight(1f)\r\n    )\r\n    Column(\r\n        modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center).weight(1f),\r\n        horizontalGravity = Alignment.CenterHorizontally,\r\n        verticalArrangement = Arrangement.spacedBy(space = 40.dp)\r\n    ) {\r\n        Text(text = selectedImageModel.id, fontSize = 60.sp)\r\n        Image(asset = imageResource(selectedImageModel.image))\r\n    }\r\n}\r\n<\/pre>\n<p>\n  Building the different layouts on two screens can be tricky, but the code included above does it in just a few lines. A Composable <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/foundation\/layout\/package-summary#row\">Row<\/a> is created, which is a horizontal list, with two columns to hold the layout for each screen, a vertical list on one screen, and a view with a full image on the other. Then setting a proper <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/foundation\/layout\/RowScope#weight\">weight<\/a> is the key point. <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/foundation\/layout\/RowScope#weight\">Weight<\/a> is a functionality in <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/ui\/Modifier\">Modifier<\/a>, which is used to divide the vertical\/horizontal space according to the assigned value. Here, the value is set to<strong> 1 <\/strong>for both columns, meaning that both will be given the same weight. The parent <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/foundation\/layout\/package-summary#row\">Row<\/a> will be divided into half and aligns the two columns equally with the same space.\n<\/p>\n<p><strong>Feedback<\/strong>\n<\/p>\n<p>\n  We hope you\u2019ve got some ideas about Jetpack Compose and its development on Surface Duo. Jetpack Compose is actively developed, so we will keep our sample up-to-date and show you more about how to use it for Surface Duo development.\n<\/p>\n<p>\nAll our sample code is open source. Feel free to try the <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-samples\/\">samples on GitHub<\/a> and contribute your ideas. We\u2019d love to hear from you! Please leave us feedback using our\u00a0<a href=\"http:\/\/aka.ms\/SurfaceDuoSDK-Feedback\">feedback forum<\/a>, or message me on <a href=\"https:\/\/twitter.com\/joyl1216\">Twitter<\/a> or <a href=\"https:\/\/github.com\/joyl1216\">GitHub<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hello, Android dual-screen developers! Today we are going to talk about how to use the new UI framework, Jetpack Compose to build a dual-screen app on the Surface Duo. Jetpack Compose is a new Declarative UI Framework in Android. Instead of using the traditional XML layouts, the developer calls the Composable functions to get the [&hellip;]<\/p>\n","protected":false},"author":30456,"featured_media":685,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[692,473,46],"class_list":["post-684","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-surface-duo-sdk","tag-jetpack-compose","tag-kotlin","tag-surface-duo"],"acf":[],"blog_post_summary":"<p>Hello, Android dual-screen developers! Today we are going to talk about how to use the new UI framework, Jetpack Compose to build a dual-screen app on the Surface Duo. Jetpack Compose is a new Declarative UI Framework in Android. Instead of using the traditional XML layouts, the developer calls the Composable functions to get the [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts\/684","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=684"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts\/684\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/media\/685"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/media?parent=684"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/categories?post=684"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/tags?post=684"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}