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.

Pietro F. Maggi

November 11, 2021
Tweet

More Decks by Pietro F. Maggi

Other Decks in Programming

Transcript

  1. Pietro F. Maggi (he/him)
    Android Developer Relations Engineer
    @pfmaggi
    Building Android UIs
    for any screen size

    View Slide

  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

    View Slide

  3. bit.ly/ADS21-LS
    Building for Large Screens
    ADS21 Playlist

    View Slide

  4. Design
    Guidance
    Views Jetpack
    Compose
    Testing

    View Slide

  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

    View Slide

  6. Large screens are growing in reach
    265%
    Growth in Foldables
    12 month growth
    2: IDC Quarterly Mobile Phone Tracker, 2021Q2
    2

    View Slide

  7. Build your app for the
    ecosystem,
    increase your reach

    View Slide

  8. View Slide

  9. View Slide

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

    View Slide

  11. Jetpack
    WindowManager
    Android 12L

    View Slide

  12. Jetpack WindowManager 1.0
    Works with 12L and prior platform
    versions to bring organic large-screen
    support to your apps.

    View Slide

  13. 2020 | Confidential and Proprietary
    WindowMetrics
    maxWindowMetrics
    currentWindowMetrics
    ● Building Android UIs for any screen size

    View Slide

  14. class MyActivity : Activity() {
    override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    val windowMetrics = WindowMetricsCalculator.getOrCreate()
    .computeCurrentWindowMetrics(this)
    }
    }
    WindowMetrics

    View Slide

  15. container.addView(object : View(this) {
    override fun onConfigurationChanged(newConfig: Configuration?)
    {
    super.onConfigurationChanged(newConfig)
    logCurrentWindowMetrics("Config.Change")
    }
    })
    WindowMetrics

    View Slide

  16. View Slide

  17. View Slide

  18. Folding screens

    View Slide

  19. FoldingFeature
    Orientation: Vertical
    State: Flat
    Separating: true
    Occlusion: Full
    Orientation: Horizontal
    State: Half-folded
    Separating: true
    Occlusion: None

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  23. private fun isTabletopPosture(foldFeature: FoldingFeature) =
    foldFeature.state == FoldingFeature.State.HALF_OPENED &&
    foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
    Tabletop

    View Slide

  24. WindowManager
    Testing

    View Slide

  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

    View Slide

  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

    View Slide

  27. @Test
    public fun testMyFeature() {
    val feature = TestWindowLayoutInfo(emptyList())
    publisherRule.overrideWindowLayoutInfo(feature)
    activityRule.scenario.onActivity { activity ->
    // App Specific Test
    }
    }
    Test WindowLayoutInfo

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  31. Activity
    Embedding

    View Slide

  32. Side-by-side activities

    View Slide

  33. window:splitRatio="0.3"
    window:splitMinWidth="600dp"
    window:finishPrimaryWithSecondary="true"
    window:finishSecondaryWithPrimary="true">
    window:primaryActivityName=".SplitActivityList"
    window:secondaryActivityName=".SplitActivityDetail"/>
    window:primaryActivityName="*"
    window:secondaryActivityName="*/*"
    window:secondaryActivityAction="android.intent.action.VIEW"/>

    main_split_config.xml

    View Slide

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

    main_split_config.xml

    View Slide

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

    main_split_config.xml

    View Slide

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

    main_split_config.xml

    View Slide

  37. More information
    ● Available with 12L Emulator
    ● Experimental in Jetpack WindowManager v1.0.0-beta03+
    d.android.com/guide/topics/large-screens/activity-embedding

    View Slide

  38. Additional resources
    ● goo.gle/foldables
    ● Best practices for video apps on
    foldables and large screens

    View Slide

  39. Build your app
    with adaptive UI

    View Slide

  40. Design
    Guidance
    Views Jetpack
    Compose
    Improved
    Testing

    View Slide

  41. Window Size
    Classes

    View Slide

  42. View Slide

  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

    View Slide

  44. View Slide

  45. View Slide

  46. Window Size Classes
    Opinionated viewport breakpoints
    Guidance Tools
    APIs

    View Slide

  47. Reference devices

    View Slide

  48. View Slide

  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

    View Slide

  50. goo.gle/window-size-classes
    goo.gle/jetnews

    View Slide

  51. Design
    Adaptive UI

    View Slide

  52. View Slide

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

    View Slide

  54. View Slide

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

    View Slide

  56. Design
    Guidance
    Views Jetpack
    Compose
    Testing

    View Slide

  57. Views
    Adaptive UI

    View Slide

  58. View Slide

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

    View Slide

  60. View Slide

  61. Trackr Navigation Graph
    Tasks Details Edit/ New Se ings Archive
    Bo om App Bar
    New Task

    View Slide

  62. bit.ly/ADS21-LS
    Building for Large Screens
    ADS21 Playlist

    View Slide

  63. Two Pane
    SlidingPaneLayout

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  67. bit.ly/ADS21-LS
    Building for Large Screens
    ADS21 Playlist

    View Slide

  68. Design
    Guidance
    Views Jetpack
    Compose
    Testing

    View Slide

  69. Jetpack Compose
    Adaptive UI

    View Slide

  70. @Composable
    fun Card() {
    if (small) {
    Column {
    Title(...)
    Subtitle(...)
    }
    } else {
    Column {
    Image(...)
    Title(...)
    Subtitle(...)
    }
    }
    }
    Jetpack Compose

    View Slide

  71. goo.gle/jetnews

    View Slide

  72. View Slide

  73. View Slide

  74. Window Size
    Classes

    View Slide

  75. View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  81. bit.ly/ADS21-LS
    Building for Large Screens
    ADS21 Playlist

    View Slide

  82. Design
    Guidance
    Views Jetpack
    Compose
    Testing

    View Slide

  83. Testing
    Adaptive UI

    View Slide

  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

    View Slide

  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"
    }
    }
    }

    View Slide

  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)
    }
    }
    }
    }

    View Slide

  87. $ gradlew
    -Pandroid.experimental.androidTest.useUnifiedTestPlatform=true
    mediumAndExpandedWidthGroupDebugAndroidTest
    87

    View Slide

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

    View Slide

  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

    View Slide

  90. ATD
    AVD today
    + +
    =
    Automated Test Device (ATD)
    AVD today
    S + S + S
    =

    View Slide

  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"
    }
    }
    }

    View Slide

  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)

    View Slide

  93. Design
    Guidance
    Views Jetpack
    Compose
    Improved
    Testing

    View Slide

  94. View Slide

  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

    View Slide

  96. d.android.com/large-screens

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide