{"id":2576,"date":"2022-06-30T12:48:07","date_gmt":"2022-06-30T19:48:07","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/surface-duo\/?p=2576"},"modified":"2022-06-30T12:48:07","modified_gmt":"2022-06-30T19:48:07","slug":"jetpack-compose-video-calculator-samples","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/surface-duo\/jetpack-compose-video-calculator-samples\/","title":{"rendered":"Video+Chat and Calculator samples for Jetpack Compose"},"content":{"rendered":"<p>\n  Hello Jetpack Compose developers!\n<\/p>\n<p>\n  Today we will be discussing the introduction of two new Jetpack Compose samples for Surface Duo. The first one is a video-streaming-chat-like app, and the second is a calculator app. \n<\/p>\n<p>\n  We will also be going over some of the dos and don&#8217;ts when creating these types of apps, as well as dive into the helpfulness of using side effects in Jetpack Compose. \n<\/p>\n<h2>Video+Chat<\/h2>\n<p>\n  About a year ago we released our own <a href=\"https:\/\/github.com\/microsoft\/surface-duo-window-manager-samples\/tree\/main\/FoldingVideoPlusChat\">Video+Chat<\/a> sample written using Views, and about 3 months ago we released another app with similar tech called the <a href=\"https:\/\/github.com\/microsoft\/surface-duo-window-manager-samples\/tree\/main\/FoldingVideoPlusTrivia\">FoldingVideoPlusTrivia<\/a>. This specific example, as you would have guessed, is based on the former, ported over to Jetpack Compose. \n<\/p>\n<p>\n  <a href=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-video-single-1.png\"><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-video-single-1.png\" alt=\"Video single screen\" width=\"350\" height=\"509\" class=\"alignnone size-full wp-image-2596\" \/><\/a><br\/ ><i>Figure 1. Video+Chat single-portrait<\/i>\n<\/p>\n<p>\n  <a href=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-video-dual.png\"><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-video-dual-1024x814.png\" alt=\"Image intern video dual\" width=\"640\" height=\"509\" class=\"alignnone size-large wp-image-2588\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-video-dual-1024x814.png 1024w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-video-dual-300x238.png 300w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-video-dual-768x610.png 768w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-video-dual.png 1149w\" sizes=\"(max-width: 640px) 100vw, 640px\" \/><\/a><br\/><i>Figure 2. Video+Chat dual-portrait<\/i>\n<\/p>\n<p>\n  Although this app and its predecessor appear similar, their implementation is a lot more different than it would seem. \n<\/p>\n<p>\n  Each app makes use of the <a href=\"https:\/\/docs.microsoft.com\/dual-screen\/introduction#dual-view\">Dual View<\/a> <a href=\"https:\/\/docs.microsoft.com\/en-us\/dual-screen\/introduction#companion-pane\">Companion Pane<\/a> pattern, as well as <a href=\"https:\/\/exoplayer.dev\/\">Exoplayer<\/a> for its video. How you implement Exoplayer into your app will make or break the experience. \n<\/p>\n<p>\n  Disclaimer: failing to follow these dos and don&#8217;ts may lead to the unfortunate passing of keyboards, mice, and even monitors as you throw your head wildly into objects out of frustration. You have been warned&#8230; \n<\/p>\n<h2>My \u201cExoplayer in Jetpack Compose\u201d <a href=\"https:\/\/www.youtube.com\/watch?v=HtTUsOKjWyQ\">Experience<\/a><\/h2>\n<\/p>\n<p>\n  One of the most important parts of embedding Exoplayer into your application is to allow it to work alongside the <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/lifecycle\">Compose Lifecycle<\/a>, rather than the <a href=\"https:\/\/developer.android.com\/guide\/components\/activities\/activity-lifecycle\">Activity Lifecyle<\/a> as you previously might have configured.\n<\/p>\n<p>\n  Because Exoplayer is embedded into a Composable as opposed to a View, attempting to follow the Activity Lifecycle rather than the Compose Lifecycle will result in adverse behaviors such as:\n<\/p>\n<ul>\n<li>\n    Multiple sources of audio playing\n  <\/li>\n<li>\n    Audio and scrollbar continuing, but video is now a black screen\n  <\/li>\n<li>\n    Video and audio breaking completely\n  <\/li>\n<\/ul>\n<p>\n  Yeah yeah, hindsight is 20 20. This is an obvious choice now, but one I did NOT make when I started this project. Which leads me to our dos and don&#8217;ts.\n<\/p>\n<h3>Do:<\/h3>\n<pre>@Composable\r\nfun Video(modifier: Modifier, currentPosition: Long, updatePosition: (Long) -&gt; Unit) {\r\n    val context = LocalContext.current\r\n    val mediaItem =\r\n        MediaItem.fromUri(\"https:\/\/storage.googleapis.com\/exoplayer-test-media-0\/BigBuckBunny_320x180.mp4\")\r\n\r\n    val player = remember {\r\n    ExoPlayer.Builder(context).build().apply {\r\n            this.setMediaItem(mediaItem)\r\n            this.playWhenReady = true\r\n            this.prepare()\r\n        }\r\n    }\r\n\r\n    DisposableEffect(\r\n        key1 = AndroidView(\r\n        modifier = modifier,\r\n        factory = {\r\n                StyledPlayerView(context).apply {\r\n                    this.player = player\r\n                setShowPreviousButton(false)\r\n                setShowNextButton(false)\r\n            }\r\n        },\r\n        update = {\r\n            player.seekTo(currentPosition)\r\n        }\r\n        )\r\n    ) {\r\n    onDispose {\r\n        updatePosition(player.currentPosition)\r\n        player.release()\r\n    }\r\n  }\r\n}<\/pre>\n<h3>Don\u2019t:<\/h3>\n<pre>\r\n  MainActivity.kt\r\n\r\n...\r\n\r\noverride fun onCreate(savedInstanceState: Bundle?) {\r\n      super.onCreate(savedInstanceState)\r\n      player = ExoPlayer.Builder(this<a id=\"post-2576-_Int_IsOl6Prn\"><\/a>).build()\r\n      setContent {\r\n\r\n        MainPage(..., player = player)\r\n\r\n...\r\n\r\noverride fun onStart() {\r\n      super.onStart()\r\n      val mediaItem =\r\n          MediaItem.fromUri(\"https:\/\/storage.googleapis.com\/exoplayer-test-media-0\/BigBuckBunny_320x180.mp4\")\r\n      player.setMediaItem(mediaItem)\r\n      player.prepare()\r\n  }\r\n\r\n  companion object {\r\n      const val STATE_PLAY_WHEN_READY = \"playerPlayWhenReady\"\r\n      const val STATE_CURRENT_POSITION = \"playerPlaybackPosition\"\r\n  }\r\n\r\n  override fun onRestoreInstanceState(savedInstanceState: Bundle) {\r\n      super.onRestoreInstanceState(savedInstanceState)\r\n\r\n      player.playWhenReady = savedInstanceState.getBoolean(STATE_PLAY_WHEN_READY)\r\n      player.seekTo(savedInstanceState.getLong(STATE_CURRENT_POSITION))\r\n      player.prepare()\r\n  }<\/pre>\n<pre>VideoPage.kt\r\n\r\n@Composable\r\nfun Video(modifier: Modifier, player: ExoPlayer) {\r\n    val context = LocalContext.current\r\n    AndroidView(\r\n        modifier = modifier,\r\n        factory = {\r\n            StyledPlayerView(context).apply {\r\n                this.player = player\r\n                setShowPreviousButton(false)\r\n                setShowNextButton(false)\r\n            }\r\n        }\r\n    )\r\n}<\/pre>\n<p>\n  Because Activity lifecycles do not connect to their associated Composable lifecycles, when the Exoplayer that is embedded deep into a Composable recomposes, the callbacks that reset the Exoplayer will NOT be called and the Exoplayer will descend into the behaviors mentioned above. \n<\/p>\n<p>So, if we don\u2019t manage the state of the Exoplayer with normal Activity callbacks, how do we manage it?\n<\/p>\n<p>\n  Good question, we do that with <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/state\">States<\/a> and <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/side-effects\">Side Effects<\/a>.\n<\/p>\n<p>\n  Namely, we save all the goodies, such as the current position of the video, into a state object. The currentPosition value that we save is the position in time of the video. We remember the  position by calling remembersavable to save the value on recomposition. After that, we can then monitor and handle the changes in the composition of the Composable the Exoplayer is embedded into with <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/side-effects\">DisposedEffect<\/a>.\n<\/p>\n<p>\n  DisposedEffect allows us to set its key1 field to the AndroidView that houses our Exoplayer. Once that View changes, DisposedEffect will run whatever is in its effect field. Currently I do not have anything set to that field, but it could be used to pause the video player or set it to some other time. Nevertheless, the important thing is that when the composable is removed from the <a id=\"post-2576-_Int_EBcIu85f\"><\/a>UI, we manage the release of the video player using the onDispose function. This removes any issue of multiple streams of video playing at once, but also allows for us to safely start up the video player once the composable re-enters the UI. \n<\/p>\n<p>\n  One of the more interesting techniques that were used here was setting the full screen. Beforehand, we would have to set the size or bounds of the composable or view to fit the entire screen, then adjust it back to its original when we exit fullscreen. How I carried out full screen HERE however was a little different. I mainly made use of the panemode in TwoPaneLayout, which is an orientation mode that sets how pane1 will span over multiple screens. \n<\/p>\n<ul>\n<li><b>TwoPane<\/b> &#8211; Normal dual layout, pane1 content on pane1, pane2 on pane2\n<\/li>\n<li><b>VerticalSingle<\/b> &#8211; When the fold is vertical (dual portrait) stretch pane1 across both panes\n<\/li>\n<li>\n  <b>HorizontalSingle<\/b> &#8211; Same as vertical, but when fold is horizontal\n<\/li>\n<\/ul>\n<p>\n  Set your <code>panemode<\/code> to get different orientations, or dynamically set it to allow for the screen to be fullscreen.\n<\/p>\n<pre>val paneMode = if (isFullScreen) TwoPaneMode.VerticalSingle else TwoPaneMode.TwoPane\r\n\r\nTwoPaneLayout(\r\n    paneMode = paneMode,\r\n    pane1 = {\r\n        if (isFullScreen) {\r\n            VideoPage(...)\r\n...<\/pre>\n<p>\n  Other than that, go wild. The app here, if you are unfamiliar with its View clone, depicts how one might build a video app with a chat alongside it. But given the fact that you can embed a video in this way, you can simply treat the video as a composable and toss it around your app. One idea I had originally was using more of the weight functionality in the <a href=\"https:\/\/docs.microsoft.com\/dual-screen\/android\/jetpack\/compose\/two-pane-layout\">TwoPaneLayout<\/a> UI component, adding a gesture to the chat where it will modify the weights of each composable, and allow for users to swipe the chat in and out of View.\n<\/p>\n<h2>Calculator<\/h2>\n<p>\n  On the other hand, the calculator is a more Jetpack Compose styling type example. The calculator functions as you would think it does. However, unlike other apps, to access the more advanced equations, the user must put the device in dual mode. \n<\/p>\n<p>\n  We also call our calculator sample \u201cDyAdd,\u201d Dyad for the duo synonym, and Add because, well, Addition. \n<\/p>\n<p>\n  <a href=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-single.png\"><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-single.png\" alt=\"Calculator on a single screen\" width=\"350\" height=\"509\" class=\"alignnone size-full wp-image-2585\" \/><\/a><br\/><i>Figure 3. DyAdd single-portrait<\/i>\n<\/p>\n<p>\n  <a href=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-single-wide.png\"><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-single-wide.png\" alt=\"Calculator on single screen landscape\" width=\"700\" height=\"509\" class=\"alignnone size-full wp-image-2586\" \/><\/a><br\/><i>Figure 4. DyAdd single-landscape<\/i>\n<\/p>\n<p>\n  As you can see, only basic functionality is available with this app when it is in single portrait mode, and even less functionality is available in single landscape.\n<\/p>\n<p>\n  With the shift over to dual mode, not only does the user get to access the more advanced equations, but the ticker tape also moves locations. \n<\/p>\n<p>\n  <a href=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-dual.png\"><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-dual.png\" alt=\"Calculator on two screens\" width=\"1149\" height=\"913\" class=\"alignnone size-full wp-image-2584\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-dual.png 1149w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-dual-300x238.png 300w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-dual-1024x814.png 1024w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-dual-768x610.png 768w\" sizes=\"(max-width: 1149px) 100vw, 1149px\" \/><\/a><br\/><i>Figure 5. DyAdd dual-portrait<\/i>\n<\/p>\n<p>\n  <a href=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-tall.png\"><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-tall.png\" alt=\"Calculator on two screens\" width=\"500\"  class=\"alignnone size-full wp-image-2587\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-tall.png 913w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-tall-238x300.png 238w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-tall-814x1024.png 814w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2022\/06\/intern-calc-tall-768x967.png 768w\" sizes=\"(max-width: 913px) 100vw, 913px\" \/><\/a><br\/><i>Figure 6. DyAdd dual-landscape<\/i>\n<\/p>\n<p>\n  You will also notice that with the configuration change, the ticker tape goes from displaying the most recent calculation on the bottom to displaying on the top.\n<\/p>\n<p>\n  I accomplish this by following the <a href=\"https:\/\/docs.microsoft.com\/dual-screen\/introduction#extended-canvas\">Extended Canvas<\/a> pattern and using <a href=\"https:\/\/docs.microsoft.com\/dual-screen\/android\/jetpack\/compose\/two-pane-layout\">TwoPaneLayout.<\/a>\n<\/p>\n<pre>@Composable\r\nfun MainPage(windowState: WindowState) {\r\n    TwoPaneLayout(\r\n        pane1 = { BasicCalculatorPage(windowState = windowState) },\r\n        pane2 = { AdvancedCalculatorPage(windowState = windowState) }\r\n    )\r\n}<\/pre>\n<p>\n  I then pass in the <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-sdk\/tree\/main\/WindowState\">WindowState<\/a> to each page, allowing for each page to determine which Composables to call depending on the orientation of the device. <code>BasicCalculatorPage<\/code> and <code>AdvancedCalculatorPage<\/code> represent the different UIs showing in each pane.\n<\/p>\n<pre>@Composable\r\nfun BasicCalculatorPage(windowState: WindowState) {\r\n    if (windowState.isSinglePortrait() || windowState.isDualPortrait()) {\r\n        BasicCalculatorWithHistory()\r\n    } else {\r\n        BasicCalculator()\r\n    }\r\n}\r\n\r\n@Composable\r\nfun AdvancedCalculatorPage(windowState: WindowState) {\r\n    if (windowState.isDualLandscape()) {\r\n        AdvancedCalculatorWithHistory()\r\n    } else {\r\n        AdvancedCalculator()\r\n    }\r\n}<\/pre>\n<p>\n  This is a really easy way to set up your Jetpack Compose app to have dynamic and interesting ways to let users experience your app. You really can go as deep as you want with this thing, even with this example you could drill the WindowState down to each grid component and re-order how they look. I simply gave each grid a column count to change how many columns were shown.\n<\/p>\n<pre>  @Composable\r\n  fun AdvancedEquationGrid(columnCount: Int, modifier: Modifier = Modifier) {\r\n      LazyVerticalGrid(\r\n          cells = GridCells.Fixed(columnCount),\r\n\r\n...<\/pre>\n<p>\n  But you could also just pass the WindowState here and rearrange the ordering of the buttons to, say, have all the trig functions right next to each other or move all the number buttons next to or inside another grid. The sky is your limit with this stuff. \n<\/p>\n<p>\n  If you\u2019re interested in checking out the source code for these projects, feel free to mosey on over <a href=\"https:\/\/github.com\/microsoft\/surface-duo-compose-samples\">here.<\/a>\n<\/p>\n<p>\n  Happy Coding \ud83d\ude0a\n<\/p>\n<h2>\n  Resources and feedback\n<\/h2>\n<p>\n  If you have any questions or would like to tell us about your dual-screen applications, use the\u00a0<a href=\"http:\/\/aka.ms\/SurfaceDuoSDK-Feedback\" target=\"_blank\" rel=\"noopener\">feedback forum<\/a>\u00a0or message us on Twitter\u00a0<a href=\"https:\/\/twitter.com\/surfaceduodev\" target=\"_blank\" rel=\"noopener\">@surfaceduodev<\/a>.\u00a0\n<\/p>\n<p>\n  Finally, please join us every Friday on\u00a0<a href=\"https:\/\/twitch.tv\/surfaceduodev\" target=\"_blank\" rel=\"noopener\">Twitch<\/a>\u00a0at 11am Pacific time to chat about Surface Duo developer topics! <\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hello Jetpack Compose developers! Today we will be discussing the introduction of two new Jetpack Compose samples for Surface Duo. The first one is a video-streaming-chat-like app, and the second is a calculator app. We will also be going over some of the dos and don&#8217;ts when creating these types of apps, as well as [&hellip;]<\/p>\n","protected":false},"author":94342,"featured_media":2584,"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-2576","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! Today we will be discussing the introduction of two new Jetpack Compose samples for Surface Duo. The first one is a video-streaming-chat-like app, and the second is a calculator app. We will also be going over some of the dos and don&#8217;ts when creating these types of apps, as well as [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts\/2576","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\/94342"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/comments?post=2576"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts\/2576\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/media\/2584"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/media?parent=2576"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/categories?post=2576"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/tags?post=2576"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}