Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building Android UIs for any screen size

Building Android UIs for any screen size

The rising popularity of tablets and foldable devices unlocks new opportunities to address a new range of users in new ways. A responsive UI allows to target these users in a new and engaging way.
In this talk you’ll get an understanding of what’s available for developers to support large screens. We’ll cover new APIs like Compose for large screens and Jetpack WindowManager, Android Studio templates and more. By the end of this talk, you’ll have all the skills you need to create and test responsive UIs on Android so that users love your app no matter what device they’re using it on.

3dfa913cbcfe896af79e0484084c316b?s=128

Pietro F. Maggi

November 11, 2021
Tweet

Transcript

  1. Pietro F. Maggi (he/him) Android Developer Relations Engineer @pfmaggi Building

    Android UIs for any screen size
  2. • Jetpack Compose 1.1 beta • Window Size Classes new

    • Embedded Activity new • SlidingPaneLayout ◦ Navigation beta ◦ Preferences alpha • 12L feature drop new • Material Design Adaptive Guidance new • Reference Devices new • Resizeable Emulator new • Visual Linting new
  3. bit.ly/ADS21-LS Building for Large Screens ADS21 Playlist

  4. Design Guidance Views Jetpack Compose Testing

  5. Large screens are growing in reach 100M New Android tablets

    92% YoY ChromeOS growth 250M Active large screen Android devices today 12 month growth 1: IDC Quarterly Personal Computing Device Tracker, 2021Q2 1
  6. Large screens are growing in reach 265% Growth in Foldables

    12 month growth 2: IDC Quarterly Mobile Phone Tracker, 2021Q2 2
  7. Build your app for the ecosystem, increase your reach

  8. None
  9. None
  10. bit.ly/ADS21-LS Building for Large Screens ADS21 Playlist

  11. Jetpack WindowManager Android 12L

  12. Jetpack WindowManager 1.0 Works with 12L and prior platform versions

    to bring organic large-screen support to your apps.
  13. 2020 | Confidential and Proprietary WindowMetrics maxWindowMetrics currentWindowMetrics • Building

    Android UIs for any screen size
  14. class MyActivity : Activity() { override fun onConfigurationChanged(newConfig: Configuration) {

    super.onConfigurationChanged(newConfig) val windowMetrics = WindowMetricsCalculator.getOrCreate() .computeCurrentWindowMetrics(this) } } WindowMetrics
  15. container.addView(object : View(this) { override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig)

    logCurrentWindowMetrics("Config.Change") } }) WindowMetrics
  16. None
  17. None
  18. Folding screens

  19. FoldingFeature Orientation: Vertical State: Flat Separating: true Occlusion: Full Orientation:

    Horizontal State: Half-folded Separating: true Occlusion: None
  20. val windowInfoRepo = windowInfoRepository() lifecycleScope.launch(Dispatchers.Main) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { windowInfoRepo.windowLayoutInfo .collect

    { newLayoutInfo -> updateCurrentState(newLayoutInfo) } } } Folding Feature
  21. val windowInfoRepo = windowInfoRepository() lifecycleScope.launch(Dispatchers.Main) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { windowInfoRepo.windowLayoutInfo .collect

    { newLayoutInfo -> updateCurrentState(newLayoutInfo) } } } Folding Feature
  22. val windowInfoRepo = windowInfoRepository() lifecycleScope.launch(Dispatchers.Main) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { windowInfoRepo.windowLayoutInfo .collect

    { newLayoutInfo -> updateCurrentState(newLayoutInfo) } } } Folding Feature
  23. private fun isTabletopPosture(foldFeature: FoldingFeature) = foldFeature.state == FoldingFeature.State.HALF_OPENED && foldFeature.orientation

    == FoldingFeature.Orientation.HORIZONTAL Tabletop
  24. WindowManager Testing

  25. val testFeature = FoldingFeature( activity: Activity, center: Int = -1,

    size: Int = 0, state: State = HALF_OPENED, orientation: Orientation = VERTICAL, ) val expected = TestWindowLayoutInfo(listOf(feature)) Test instances
  26. private val activityRule = ActivityScenarioRule(TestActivity::class.java) private val publisherRule = WindowLayoutInfoPublisherRule()

    @get:Rule public val testRule: TestRule init { testRule = RuleChain.outerRule(publisherRule).around(activityRule) } Test rules
  27. @Test public fun testMyFeature() { val feature = TestWindowLayoutInfo(emptyList()) publisherRule.overrideWindowLayoutInfo(feature)

    activityRule.scenario.onActivity { activity -> // App Specific Test } } Test WindowLayoutInfo
  28. @Test fun testDeviceOpen_TableTop() { activityRule.scenario.onActivity { activity -> val feature

    = FoldingFeature(activity = activity, state = HALF_OPENED, orientation = HORIZONTAL) val expected = TestWindowLayoutInfo(listOf(feature)) publisherRule.overrideWindowLayoutInfo(expected) } onView(withSubstring("state = HALF_OPENED")).check(matches(isDisplayed())) onView(withSubstring("are separated")).check(matches(isDisplayed())) onView(withSubstring("Hinge is horizontal")).check(matches(isDisplayed())) } Test WindowLayoutInfo
  29. @Test fun testDeviceOpen_TableTop() { activityRule.scenario.onActivity { activity -> val feature

    = FoldingFeature(activity = activity, state = HALF_OPENED, orientation = HORIZONTAL) val expected = TestWindowLayoutInfo(listOf(feature)) publisherRule.overrideWindowLayoutInfo(expected) } onView(withSubstring("state = HALF_OPENED")).check(matches(isDisplayed())) onView(withSubstring("are separated")).check(matches(isDisplayed())) onView(withSubstring("Hinge is horizontal")).check(matches(isDisplayed())) } Test WindowLayoutInfo
  30. @Test fun testDeviceOpen_TableTop() { activityRule.scenario.onActivity { activity -> val feature

    = FoldingFeature(activity = activity, state = HALF_OPENED, orientation = HORIZONTAL) val expected = TestWindowLayoutInfo(listOf(feature)) publisherRule.overrideWindowLayoutInfo(expected) } onView(withSubstring("state = HALF_OPENED")).check(matches(isDisplayed())) onView(withSubstring("are separated")).check(matches(isDisplayed())) onView(withSubstring("Hinge is horizontal")).check(matches(isDisplayed())) } Test WindowLayoutInfo
  31. Activity Embedding

  32. Side-by-side activities

  33. <SplitPairRule window:splitRatio="0.3" window:splitMinWidth="600dp" window:finishPrimaryWithSecondary="true" window:finishSecondaryWithPrimary="true"> <SplitPairFilter window:primaryActivityName=".SplitActivityList" window:secondaryActivityName=".SplitActivityDetail"/> <SplitPairFilter window:primaryActivityName="*"

    window:secondaryActivityName="*/*" window:secondaryActivityAction="android.intent.action.VIEW"/> </SplitPairRule> main_split_config.xml
  34. <SplitPairRule window:splitRatio="0.3" window:splitMinWidth="600dp" window:finishPrimaryWithSecondary="true" window:finishSecondaryWithPrimary="true"> <SplitPairFilter window:primaryActivityName=".SplitActivityList" window:secondaryActivityName=".SplitActivityDetail"/> <SplitPairFilter window:primaryActivityName="*"

    window:secondaryActivityName="*/*" window:secondaryActivityAction="android.intent.action.VIEW"/> </SplitPairRule> main_split_config.xml
  35. <SplitPairRule window:splitRatio="0.3" window:splitMinWidth="600dp" window:finishPrimaryWithSecondary="true" window:finishSecondaryWithPrimary="true"> <SplitPairFilter window:primaryActivityName=".SplitActivityList" window:secondaryActivityName=".SplitActivityDetail"/> <SplitPairFilter window:primaryActivityName="*"

    window:secondaryActivityName="*/*" window:secondaryActivityAction="android.intent.action.VIEW"/> </SplitPairRule> main_split_config.xml
  36. <SplitPairRule window:splitRatio="0.3" window:splitMinWidth="600dp" window:finishPrimaryWithSecondary="true" window:finishSecondaryWithPrimary="true"> <SplitPairFilter window:primaryActivityName=".SplitActivityList" window:secondaryActivityName=".SplitActivityDetail"/> <SplitPairFilter window:primaryActivityName="*"

    window:secondaryActivityName="*/*" window:secondaryActivityAction="android.intent.action.VIEW"/> </SplitPairRule> main_split_config.xml
  37. More information • Available with 12L Emulator • Experimental in

    Jetpack WindowManager v1.0.0-beta03+ d.android.com/guide/topics/large-screens/activity-embedding
  38. Additional resources • goo.gle/foldables • Best practices for video apps

    on foldables and large screens
  39. Build your app with adaptive UI

  40. Design Guidance Views Jetpack Compose Improved Testing

  41. Window Size Classes

  42. None
  43. class WindowMetrics { class WindowSizeClass(val name: String) { companion object

    { val COMPACT = WindowSizeClass("COMPACT") val MEDIUM = WindowSizeClass("MEDIUM") val EXPANDED = WindowSizeClass("EXPANDED") } } val widthClass: WindowSizeClass get() { ... } val heightClass: WindowSizeClass get() { ... } } Window Size Classes (subject to change) Window Manager
  44. None
  45. None
  46. Window Size Classes Opinionated viewport breakpoints Guidance Tools APIs

  47. Reference devices

  48. None
  49. Resizable UI checklist ❏ Optimize your application’s layout for ‘Compact’

    and ‘Expanded’ widths ❏ Test it across all reference devices, and pick the better layout for ‘Medium’ ❏ Consider additional improvements, including custom layouts and input support
  50. goo.gle/window-size-classes goo.gle/jetnews

  51. Design Adaptive UI

  52. None
  53. Material Design guidance material.io/design/layout

  54. None
  55. bit.ly/ADS21-LS ADS21 Playlist Design beautiful apps on foldables and large

    screens
  56. Design Guidance Views Jetpack Compose Testing

  57. Views Adaptive UI

  58. None
  59. https://github.com/android/trackr

  60. None
  61. Trackr Navigation Graph Tasks Details Edit/ New Se ings Archive

    Bo om App Bar New Task
  62. bit.ly/ADS21-LS Building for Large Screens ADS21 Playlist

  63. Two Pane SlidingPaneLayout

  64. Trackr Navigation Graph Tasks Details Edit/ New Se ings Archive

    NavigationRailView New Task
  65. Trackr Navigation Graph TwoPaneTasks Details Edit/ New Se ings Archive

    New Task Edit Task NavigationRailView
  66. Trackr Navigation Graph TwoPaneTasks Details Edit/ New Se ings Archive

    New Task Edit Task NavigationRailView
  67. bit.ly/ADS21-LS Building for Large Screens ADS21 Playlist

  68. Design Guidance Views Jetpack Compose Testing

  69. Jetpack Compose Adaptive UI

  70. @Composable fun Card() { if (small) { Column { Title(...)

    Subtitle(...) } } else { Column { Image(...) Title(...) Subtitle(...) } } } Jetpack Compose
  71. goo.gle/jetnews

  72. None
  73. None
  74. Window Size Classes

  75. None
  76. @Composable fun Activity.rememberWindowSizeClass(): WindowSize { val configuration = LocalConfiguration.current val

    windowMetrics = remember(configuration) { WindowMetricsCalculator.getOrCreate() .computeCurrentWindowMetrics(this) } val windowDpSize = with(LocalDensity.current) { windowMetrics.bounds.toComposeRect().size.toDpSize() } when { windowDpSize.width < 600.dp -> WindowSize.Compact windowDpSize.width < 840.dp -> WindowSize.Medium else -> WindowSize.Expanded } } Window Size Classes
  77. @Composable fun Activity.rememberWindowSizeClass(): WindowSize { val configuration = LocalConfiguration.current val

    windowMetrics = remember(configuration) { WindowMetricsCalculator.getOrCreate() .computeCurrentWindowMetrics(this) } val windowDpSize = with(LocalDensity.current) { windowMetrics.bounds.toComposeRect().size.toDpSize() } when { windowDpSize.width < 600.dp -> WindowSize.Compact windowDpSize.width < 840.dp -> WindowSize.Medium else -> WindowSize.Expanded } } Window Size Classes
  78. @Composable fun Activity.rememberWindowSizeClass(): WindowSize { val configuration = LocalConfiguration.current val

    windowMetrics = remember(configuration) { WindowMetricsCalculator.getOrCreate() .computeCurrentWindowMetrics(this) } val windowDpSize = with(LocalDensity.current) { windowMetrics.bounds.toComposeRect().size.toDpSize() } when { windowDpSize.width < 600.dp -> WindowSize.Compact windowDpSize.width < 840.dp -> WindowSize.Medium else -> WindowSize.Expanded } } Window Size Classes
  79. @Composable fun Activity.rememberWindowSizeClass(): WindowSize { val configuration = LocalConfiguration.current val

    windowMetrics = remember(configuration) { WindowMetricsCalculator.getOrCreate() .computeCurrentWindowMetrics(this) } val windowDpSize = with(LocalDensity.current) { windowMetrics.bounds.toComposeRect().size.toDpSize() } when { windowDpSize.width < 600.dp -> WindowSize.Compact windowDpSize.width < 840.dp -> WindowSize.Medium else -> WindowSize.Expanded } } Window Size Classes
  80. @Composable fun Activity.rememberWindowSizeClass(): WindowSize { val configuration = LocalConfiguration.current val

    windowMetrics = remember(configuration) { WindowMetricsCalculator.getOrCreate() .computeCurrentWindowMetrics(this) } val windowDpSize = with(LocalDensity.current) { windowMetrics.bounds.toComposeRect().size.toDpSize() } when { windowDpSize.width < 600.dp -> WindowSize.Compact windowDpSize.width < 840.dp -> WindowSize.Medium else -> WindowSize.Expanded } } Window Size Classes
  81. bit.ly/ADS21-LS Building for Large Screens ADS21 Playlist

  82. Design Guidance Views Jetpack Compose Testing

  83. Testing Adaptive UI

  84. Gradle Managed Devices Define virtual devices and device groups using

    Gradle DSL for easy integration into CI. Device lifecycle fully managed by Android Gradle plugin (AGP) AGP downloads any required images and SDK components Caches test results and utilizes emulator snapshots for faster continuous testing Gradle Managed Devices Unified Test Platform
  85. 85 testOptions { devices { pixel2api29 (com.android.build.api.dsl.ManagedVirtualDevice) { ... }

    nexus9api30 (com.android.build.api.dsl.ManagedVirtualDevice) { device = "Nexus 9" apiLevel = 30 systemImageSource = "google" abi = "x86" } } }
  86. 86 testOptions { devices { pixel2api29 (com.android.build.api.dsl.ManagedVirtualDevice) { ... }

    nexus9api30 (com.android.build.api.dsl.ManagedVirtualDevice) { ... } deviceGroups { mediumAndExpandedWidth{ targetDevices.addAll(devices.pixel2api29) targetDevices.addAll(devices.nexus9api30) } } } }
  87. $ gradlew -Pandroid.experimental.androidTest.useUnifiedTestPlatform=true mediumAndExpandedWidthGroupDebugAndroidTest 87

  88. $ gradlew -Pandroid.experimental.androidTest.numManagedDeviceShards=2 device1DebugAndroidTest 88

  89. Automated Test Devices (ATD) Headless and weightless Optimized testing Reduces

    CPU and Memory usage and disables hardware rendering. You can still perform normal screenshot testing using AndroidX.Test. Optimizes apps and services to only those typically required for instrumented testing. From versatile device profiles to Snapshots, Gradle Managed Devices, and Test Sharding, many of the great benefits of Emulators are still available. Familiar functionality
  90. ATD AVD today + + = Automated Test Device (ATD)

    AVD today S + S + S =
  91. 91 testOptions { devices { pixel2api29 (com.android.build.api.dsl.ManagedVirtualDevice) { ... }

    nexus9api30 (com.android.build.api.dsl.ManagedVirtualDevice) { device = "Nexus 9" apiLevel = 30 systemImageSource = "aosp-atd" // or "google-atd" abi = "x86" } } }
  92. // Tests synchronous screen rotation on a device. @Test fun

    testItOnDevice() { onDevice().setScreenOrientation(ScreenOrientation.PORTRAIT)) } // Tests on a device that supports the tabletop mode. @Test fun testItOnFoldInFoldable() { onDevice().setTableTopMode() onDevice().setFlatMode() } Device Controller APIs (Preview soon)
  93. Design Guidance Views Jetpack Compose Improved Testing

  94. None
  95. • Jetpack Compose 1.1 beta • Window Size Classes new

    • Embedded Activity new • SlidingPaneLayout ◦ Navigation beta ◦ Preferences alpha • 12L feature drop new • Material Design Adaptive Guidance new • Reference Devices new • Resizeable Emulator new • Visual Linting new
  96. d.android.com/large-screens

  97. Thank you! Pietro F. Maggi (he/him) Android Developer Relations Engineer

    @pfmaggi
  98. End of deck Pietro F. Maggi (he/him) Android Developer Relations

    Engineer @pfmaggi
  99. End of deck - for real Pietro F. Maggi (he/him)

    Android Developer Relations Engineer @pfmaggi