Home Science & Technology Large Screens & Foldables Tutorial for Android

Large Screens & Foldables Tutorial for Android

71
0


Learn how to build great user experiences for large screens & foldables in Android. Also learn how to design and test adaptive Android apps.

Large screens are an important and fast-growing segment of active Android devices. There are more than 270 million large-screen Android devices in use. They include tablets, foldable devices and Chrome OS devices. To reach this growing segment of Android users, learn to make your app UI adaptive across a range of devices.

In this tutorial, you’ll build an app called Crafty Notebook, which shows a list of notes. Along the way, you’ll learn about:

  • APIs and tools to build great user experiences for large-screen Android devices.
  • Designing adaptive apps for different screen sizes, orientations and form factors.
  • Google Play updates for large-screen devices.
  • Testing your app layouts for large screens.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

In this tutorial, you’ll use a resizable emulator to test your app’s UI across different devices. To set up a resizable emulator, open the starter project with Android Studio version 2021.2.1 or newer and follow these steps:

  • Click ToolsSDK Manager. In the SDK Tools tab, select Android Emulator and click OK. This will install the latest version of the emulator if it isn’t already installed.
  • To create a virtual device, click ToolsDevice Manager. Then, click the Create device button and select PhoneResizable. Click Next and select the latest API level. Confirm the emulator details and click Finish. This will create a resizable emulator.
  • On the list of virtual devices, select the resizable device and click the Launch icon to start it.

Build and run the project. Here’s what you’ll see:

List of notes in a compact screen size

In the emulator window, click the Display Mode drop-down and select Tablet:

Click device type dropdown to switch to a different device type

On a large-screen device like a tablet, content stretches to fill the available screen space like this:

List of notes in a tablet. The UI stretches to fill the available space

But users expect a great experience while using your app across different Android devices. Your goal is to use the extra screen space to improve the user experience and provide great accessibility on large-screen devices.

Looking Into Android 12L Updates

Android devices come in various form factors: phones, tablets, foldables and Chrome OS devices. They vary in screen sizes from small to large screen sizes.

At Android Dev summit 2021, Google announced Android 12L. Android 12L is a feature update for Android 12 that was built for large-screen devices. Android 13 builds on updates made in Android 12L. Some of the updates include:

  • Taskbar interaction: The new taskbar makes it easy to launch and switch apps. Gestures such as drag and drop enter split-screen mode. In gesture navigation, users can flip through recent apps. This enables powerful and intuitive multitasking on large screens.
  • Default multi-window mode: To enhance the split screen experience, Android 12 or higher allows multi-window mode by default in all apps.
  • Improved compatibility experience: Some apps aren’t optimized for large screens yet. They aren’t resizable or are using fixed orientation. These apps are launched in compatibility mode to make them look better by default. Such apps are centered on the screen with black bars filling the unused display area.
  • Camera preview enhancements: This makes the camera app adaptive to large screens, multi-window mode and different foldable device postures.
  • Media projection updates: Starting in Android 12L, the virtual display is scaled to fit available screen space. This improves screen casting on large displays like televisions. It maximizes the size of surface images and ensures the correct aspect ratio.

You’ve learned about updates for large-screen devices starting in Android 12L. Next, you’ll learn how to build responsive apps across different devices.

Designing Adaptive Apps

Responsive apps provide a great user experience across different screen sizes and form factors. They support different screen orientations and resizable configurations like multi-window mode.

To help you create adaptive layouts, Material Design 3 provides canonical layouts. Canonical layouts serve as a guideline for creating responsive layouts for large screens. They include:

  • List-detail view: In a list-detail view, you place a list of items on the left. On the right side, you show details of an item.
  • Supporting panel: A layout consists of focus and support regions. The focus region shows the primary content. It covers two-thirds of the display area. The supporting panel occupies the remaining screen space to show additional content like comments on a document. It’s placed at the bottom third on an expanded height or trailing third on an expanded width.
  • Feed: Feed layouts are common in news or social content apps. For example, with a RecyclerView, use a different layout manager like GridLayoutManager when the width is not compact.

Knowing what kind of device the user is using won’t help you decide which app layouts to use. On tablets, for example, an app could be sharing the screen with another app in multi-window mode. Or, on a foldable device, there could be more than one physical screen. Instead, make decisions based on the actual portion of the screen that’s allocated by using Jetpack WindowManager library.

In the next section, you’ll learn how to use Window Size classes to determine the layout for your app. Window size classes are determined by the window size available to your application regardless of the type of device the app is running on.

Exploring Window Size Classes

Window size classes are viewport breakpoints to guide you in designing responsive and adaptive layouts. They classify screen space available for your app as compact, medium or expanded.

Available width and height are classified separately. The available width is more important than the available height because vertical scrolling is common across devices. The available width is classified as follows:

  • Compact width: The device width is less than 600dp. Phones in portrait orientation and foldables in folded state are in this category.
  • Medium width: The device width is more than 600dp. Medium-width devices include tablets and large unfolded foldables in portrait orientation.
  • Expanded width: Tablets and large unfolded foldables in landscape orientation fall in this category. They are more than 840dp wide.

You’ll use the material3-window-size-class library to get the window size class of a device. The library calculates the window size class using current window metrics.

Open build.gradle(app). The following library dependency has already been added:

implementation "androidx.compose.material3:material3-window-size-class:1.0.0-alpha14"

Open presentation ▸ MainActivity.kt, and replace // TODO 1 with the following:

val windowSizeClass = calculateWindowSizeClass(activity = this)

The code above returns the window size class for the provided activity. calculateWindowSizeClass(activity: Activity) calculates WindowSizeClass for the provided activity. The method returns a new WindowSizeClass during screen rotation or window resize. The app recomposes the UI with the new window size class.

Add any missing imports by pressing Option-Return on Mac or Alt-Enter on PC.

You may see an error squiggly line. This is because the library is still experimental. To fix the error, add the following before onCreate() and import the corresponding package:

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)

Next, you’ll pass windowSizeClass to the NoteApp() composable. You’ll use this information later to determine the app layouts.

Replace // TODO 2 with the following:

windowSizeClass = windowSizeClass.widthSizeClass,

Before updating the app to respond to changes in screen sizes, you’ll consider device fold posture also.

Looking Into Device Fold Posture

A foldable device can be in various states and postures. It may be folded or unfolded, in portrait or landscape orientation. It could be in a tabletop or book posture. An adaptive design supports different foldable postures.

Jetpack WindowManager library’s WindowLayoutInfo class provides the following information about foldable displays:

  • state: This describes the fold state. Its value is FLAT when the device is fully opened, or HALF_OPENED.
  • orientation: The orientation of the hinge. It can be HORIZONTAL or VERTICAL.
  • occlusionType: The value is FULL when the hinge hides part of the display. Otherwise the value is NONE.
  • isSeparating: It’s true when the hinge creates two logical displays.

You’ll use this information to determine device fold posture. Open presentation ▸ util ▸ DevicePostureUtil.kt. DevicePosture interface defines the following postures:

  • Normal posture: Whether a device is fully opened or fully folded.
  • Book posture: The device is in portrait orientation and its fold state is HALF_OPENED.
  • Separating posture: The device is completely open and its fold state is FLAT. It’s similar to the case of device posture where occlusionType is FULL because of a physical hinge. Avoid placing touchable or visible parts under the hinge.

Analyzing Device Fold Posture

To get device fold posture, open MainActivity.kt and replace // TODO 3 with the following:

// 1
val devicePostureFlow = WindowInfoTracker.getOrCreate(this).windowLayoutInfo(this)
  .flowWithLifecycle(this.lifecycle)
  // 2
  .map { layoutInfo ->
    val foldingFeature =
      layoutInfo.displayFeatures
        .filterIsInstance()
        .firstOrNull()
    when {
      isBookPosture(foldingFeature) ->
        DevicePosture.BookPosture(foldingFeature.bounds)

      isSeparating(foldingFeature) ->
        DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation)

      else -> DevicePosture.NormalPosture
    }
  }
  .stateIn(
    scope = lifecycleScope,
    started = SharingStarted.Eagerly,
    initialValue = DevicePosture.NormalPosture
  )

Also include the following imports to avoid Android Studio’s complaints:

import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import com.yourcompany.android.craftynotebook.presentation.util.DevicePosture
import com.yourcompany.android.craftynotebook.presentation.util.isBookPosture
import com.yourcompany.android.craftynotebook.presentation.util.isSeparating
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

In the code above, you’re using Kotlin Flows to work with WindowLayoutInfo data collection.

  1. windowLayoutInfo(activity: Activity) returns display information of a device as Flow. The method emits WindowLayoutInfo every time the display information changes.
  2. It uses map operator and display information returned by windowLayoutInfo(activity: Activity) to determine the device fold posture.

Next, you’ll observe device posture as compose state. In MainActivity.kt, replace // TODO 4 with the following and import the corresponding package.

val devicePosture = devicePostureFlow.collectAsState().value

Then, pass devicePosture in NoteApp() composable call. Replace // TODO 5 with the following:

devicePosture = devicePosture,

Up to this point using window size classes, the app knows the screen space available. It also knows the device fold posture. You’ll use this information to determine the app UI. First, you’ll implement responsive navigation.

Choosing Appropriate Navigation Type

Responsive UIs include different types of navigation elements corresponding to display size changes.

Material library provides navigation components like bottom navigation, navigation rail and navigation drawer. You’ll implement the most appropriate navigation depending on the window size class of a device:

  • Bottom navigation: Bottom navigation is most appropriate for compact window sizes.
  • Navigation rail: Use navigation rail for medium screen sizes.
  • Navigation drawer: This would be suitable for large-screen devices like tablets. There are two types of navigation drawers: modal and permanent. Use a modal navigation drawer for compact to medium sizes because it can be expanded as an overlay on the content or hidden. Use a permanent navigation drawer for fixed navigation on large screens like tablets and Chrome OS devices.

Now, you’ll switch between different navigation types depending on the window size of a class and device fold posture.

Open NoteApp.kt and replace // TODO 6 with the following and import the package for NavigationType:

// 1
val navigationType: NavigationType
// 2
when (windowSizeClass) {
  WindowWidthSizeClass.Compact -> {
    navigationType = NavigationType.BOTTOM_NAVIGATION
    // TODO 13
  }
  WindowWidthSizeClass.Medium -> {
    navigationType = NavigationType.NAVIGATION_RAIL
    // TODO 14
  }
  WindowWidthSizeClass.Expanded -> {
    // 3
    navigationType = if (devicePosture is DevicePosture.BookPosture) {
      NavigationType.NAVIGATION_RAIL
    } else {
      NavigationType.PERMANENT_NAVIGATION_DRAWER
    }
    // TODO 15
  }
  else -> {
    navigationType = NavigationType.BOTTOM_NAVIGATION
    // TODO 16
  }
}

The code above does the following:

  1. Declares the navigationType variable.
  2. Using a switch statement, it initializes navigationType with the correct value depending on the window size class.
  3. Handles fold state to avoid placing content or touching action at the hinge area. When a device is in BookPosture, use a navigation rail and divide content around the hinge. For large desktops or tablets, use a permanent navigation drawer.

Next, you’ll pass navigationType to NoteNavigationWrapperUi() composable call. In NoteApp.kt, replace // TODO 7 with the following:

navigationType = navigationType,

Now, the app knows navigation types to apply to different window size classes and device fold postures. Next, you’ll implement different navigation to ensure excellent interaction and reachability.

Implementing Responsive Navigation

Open NoteNavigationWrapperUi.kt. Replace NoteAppContent() composable call with the following:

if (navigationType == NavigationType.PERMANENT_NAVIGATION_DRAWER) {
  PermanentNavigationDrawer(drawerContent = {
    NavigationDrawerContent(
      navController = navController
    )
  }) {
    NoteAppContent(
      navigationType = navigationType,
      contentType = contentType,
      modifier = modifier,
      navController = navController,
      notesViewModel = notesViewModel
    )
  }
} else {
  ModalNavigationDrawer(
    drawerContent = {
      NavigationDrawerContent(
        navController = navController,
        onDrawerClicked = {
          scope.launch {
            drawerState.close()
          }
        }
      )
    },
    drawerState = drawerState
  ) {
    NoteAppContent(
      navigationType = navigationType,
      contentType = contentType,
      modifier = modifier,
      navController = navController,
      notesViewModel = notesViewModel,
      onDrawerClicked = {
        scope.launch {
          drawerState.open()
        }
      }
    )
  }
}

As usual, there are a few imports you need to add as well:

import kotlinx.coroutines.launch
import androidx.compose.material3.*

The navigation drawer is the container for notes UI. In the code above, you’re wrapping the NoteAppContent() composable call with a permanent or modal navigation drawer depending on the value of navigationType.

In NoteAppContent.kt, replace the Column() composable with the following:

Row(modifier = Modifier.fillMaxSize()) {
  AnimatedVisibility(visible = navigationType == NavigationType.NAVIGATION_RAIL) {
    NoteNavigationRail(
      onDrawerClicked = onDrawerClicked,
      navController = navController
    )
  }
  Column(
    modifier = modifier.fillMaxSize()
  ) {
    NoteNavHost(
      modifier = modifier.weight(1f),
      contentType = contentType,
      navController = navController,
      notesViewModel = notesViewModel
    )
    AnimatedVisibility(visible = navigationType == NavigationType.BOTTOM_NAVIGATION) {
      NoteBottomNavigationBar(navController = navController)
    }
  }
}

To make Android Studio happy, add the following imports as well:

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Row

The code above uses navigationType to determine placement of navigation rail or bottom navigation. You wrapped both navigation rail and bottom navigation in the AnimatedVisibility() composable. This animates the entry and exit visibility of each navigation depending on navigationType .

Build and run.

For compact window size class like a phone, the app uses bottom navigation like in the screen below:

A compact screen window size class like a phone uses bottom navigation

In a medium window size class, the app uses a navigation rail like in the screen below:

A medium window size class like unfolded foldable uses navigation rail

The app uses a permanent navigation drawer in an expanded window size class, like this:

A large screen using a permanent navigation drawer

Congratulations! You’ve successfully implemented dynamic navigation on different devices. Next, you’ll utilize the additional screen space to show more content. You’ll implement list-detail on large screens.

Displaying More Content

Open presentation ▸ util ▸ ConstantsUtil.kt. ContentType enum defines two constants: LIST_ONLY and LIST_AND_DETAIL. These will help you determine content layout depending on the window size class.

Open NoteApp.kt. Replace // TODO 12 with the following and import the corresponding package:

val contentType: ContentType

Here, you’ve declared a variable of type ContentType.

Next, you’ll initialize the contentType variable with the correct value depending on screen state.

In NoteApp.kt, replace the TODOs in the when statement as shown below:

Replace // TODO 13 with the following code:

contentType = ContentType.LIST_ONLY

It sets the value of contentType with as LIST_ONLY when the window size class is compact.

Replace // TODO 14 with the following code:

contentType = if (devicePosture is DevicePosture.BookPosture
  || devicePosture is DevicePosture.Separating
) {
  ContentType.LIST_AND_DETAIL
} else {
  ContentType.LIST_ONLY
}

The code above initializes contentType with LIST_ONLY for medium window size class. For a foldable device in book posture, set the value to LIST_DETAIL. This will separate list view and detail view at the hinge area. It helps avoid placing content or touch targets at the hinge area.

Replace // TODO 15 with the code below:

contentType = ContentType.LIST_AND_DETAIL

This sets the value of contentType as LIST_AND_DETAIL on large screens.

Finally, replace // TODO 16 with the code below to include the default case:

contentType = ContentType.LIST_ONLY

Next, pass contentType to the NoteNavigationWrapperUi() composable. Replace // TODO 17 with the following:

contentType = contentType,

You’ll use the contentType parameter to determine whether to show a list layout or a list-detail layout.

Open NotesScreen.kt. Replace the NotesListComposable() composable call with the following:

if (contentType == ContentType.LIST_AND_DETAIL) {
  NoteListDetailComposable(notes = notes)
} else {
  NotesListComposable(
    notes = notes,
    onItemSelected = onNoteItemSelected,
  )
}

The code above checks the value of contentType to determine which layout to show. The app will show a list-detail layout on large screens. In compact and medium screen sizes, the app will show a list layout.

Build and run.

On a compact or medium screen size, the app shows a list layout like in the screens below:

A compact screen size shows a list layout

A medium screen size shows a list layout

On large screens like a tablet or desktop, the app shows a list-detail layout like this:

An expanded window size class showing a list-detail view

Ensuring Data is Available for All Screen Sizes

A responsive UI retains data when a phone is rotated or a foldable is unfolded or folded. Changing phone orientation, folding and unfolding a foldable or resizing a window are configuration changes. During configuration changes, the system recreates app activities, fragments or composables. The recommended ways to preserve data across configuration changes are using a ViewModel class and rememberSavable API for compose apps.

On large screens, you may be showing more content to utilize the extra screen space. So, you may be tempted to fetch data when the screen size changes. This goes against the principle of unidirectional data flow — that state flows down and events flow up — where data should be hoisted and provided to the composables for displaying.

You should provide enough data to the composable so that it always has what it needs to display across any screen size. Then, you can use a flag like contentType to determine what data to show on a given screen size.

Open NotesScreen.kt. You’ll see code like this:

val notes = notesViewModel.notes.collectAsState().value
if (contentType == ContentType.LIST_AND_DETAIL) {
  NoteListDetailComposable(notes = notes)
} else {
  NotesListComposable(
    notes = notes,
    onItemSelected = onNoteItemSelected,
  )
}

A list of notes is passed to both NotesListComposable() and NoteListDetailComposable() composables. Full note text will not be displayed on a small or medium screen size. However, it’ll be available for displaying on large screens that show both the list of notes and details of a note item.

You’ve learned how to build adaptive apps while ensuring data is available across different device configurations. Next, you’ll learn how to test your app compatibility across different screen sizes and form factors.

Testing Apps for Large Screens With Android Studio

To test your app compatibility with large-screen devices, Android Studio provides the following capabilities:

  • Reference devices: These include phones, large foldable inner display, tablets and desktops. You can create device-respective emulators in Android Studio. Then, you’ll use the emulators to test your app layout across different devices.
  • Resizable emulator: Resizeable emulator is available in Android Studio chipmunk and higher. It lets you toggle between the four reference devices — phone, foldable, tablet and desktop — to validate your app layout at runtime.
  • Layout validation: Starting Electric Eel Canary 1, Android Studio will check for visual lint issues across different screen sizes. When you open Layout Validation, you’ll see all your layouts render in multiple device sizes. If there’s an issue, it’ll show up in the Problems Panel. Visual linting will be available for layouts written in Views or Compose.

Now, you’ll look into what to test to ensure your app compatibility in different screen sizes and form factors.

Looking Into What to Test in Large Screens

Once you’ve set up different device emulators, you’ll test your app for common use cases for large screens. Some of the things to test for include:

  • Screen sizes, device posture and orientation: Check how your app responds to changes in screen sizes, device posture of a foldable and screen orientation.
  • Taskbar integration and split screen mode: For Android 12L devices and higher, ensure your app UI isn’t blocked by the taskbar. Check your app behavior when you enter multi-window mode using the taskbar. Test switching between your app and other apps using the taskbar.
  • Multi-window mode: Check your app behavior when running in multi-window mode on large screens when android:resizeableActivity = false in AndroidManifest file. If android:resizeableActivity = true, check how your app responds when running in multi-window mode on small-screen devices.
  • Media projection: If your app uses media projection, check how your app responds while playing back, streaming or casting media on large-screen devices. Also check how the app responds to device posture changes in a foldable.
  • Camera preview: For camera apps, check how the camera preview UI responds on large screens when your app is in multi-window mode. Check how your app responds to device posture changes on a foldable device.

Checking Into Google Play Updates for Large Screens

The Android team has updated Google Play to highlight apps that are optimized for large screens. They’ve added checks to assess apps against the large screens app quality guidelines listed here. If an app isn’t optimized for large screens, users on large-screen devices will be notified on the app’s Play Store listing page.

They’ve also introduced large-screen-specific app ratings. Users can rate how your app works on their large-screen device.

Optimize your Android apps to make them easy to find on Google Play!

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

Congratulations! You have successfully made the Crafty Notebook App responsive across different devices. You have learned how to make your app adaptive to different screen sizes and form factors using Jetpack Compose. You’ve also learned how to preserve state across configuration changes and Google Play updates for large-screen devices.

Large screens are perfect for drag-and-drop interactions — within the app or between apps in multi-window mode. Check out Android Drag and Drop to learn how to add drag-and-drop capabilities to your app.

Check out this Ensuring Great Input Support for All Devices talk to learn about supporting various input methods in all devices.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Previous articleOne of the best Pixel 6 Pro Black Friday deals is back
Next articleThe Pixel 7a arrived six months early