Large Screens & Foldables
Create outstanding Android user experiences for big screens and foldable devices.
An important and quickly expanding subset of active Android smartphones have large screens. Large-screen Android devices are used on more than 270 million different devices. Tablets, foldable gadgets, and Chrome OS gadgets are among them. Make your app’s user interface (UI) responsive to a variety of devices to reach this expanding group of Android users.
You will pick up knowledge 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.
Note: This lesson assumes you have prior knowledge of Kotlin and Jetpack Compose in Android development.
Designing Adaptive Apps
A wonderful user experience is offered by responsive apps on a variety of screen sizes and form factors. They offer resizable settings including multi-window mode and various screen orientations.
Material Design 3 offers canonical layouts to aid in the creation of adaptive layouts. The creation of responsive layouts for large screens uses canonical layouts as a model. They consist of:
- List-detail view: A list of items is placed on the left in a list-detail view. You display an item’s details on the right side.
- Supporting panel: Focus and support areas make up a layout. The primary content is displayed in the focus area. It takes up around two-thirds of the display space. The remaining screen real estate is taken up by the accompanying panel, which displays extra content like document comments. It is positioned in the back third of an increased width or the bottom third of an extended height.
- Feed: News or social content apps frequently use feed layouts. When the width is not compact, for instance, use a different layout manager like GridLayoutManager with a RecyclerView.
You cannot choose the app layouts to utilize based on the type of device the user is using. On tablets, for instance, an app may be running in multi-window mode and sharing the screen with another app. Or there might be multiple physical screens on a foldable gadget. Instead, use the Jetpack WindowManager library to base choices on the actual area of the screen that is allotted.
Study of Window Size Classes
In order to help you create responsive and adaptive layouts, window size classes serve as viewport breakpoints. They categorize the available screen real estate for your app as a tiny, medium, or enlarged.
Available width and height are divided into different categories. Because vertical scrolling is prevalent across devices, the available width is more significant than the available height. The following categories apply to the available width:
- Compact width: The device width is relatively small — less than 600dp. These include phones that are held in portrait mode and folding foldable.
- Medium width: The screen’s dimensions are greater than 600dp. Tablets and huge unfolded foldable in portrait orientation are examples of medium-width devices.
- Expanded width: This category includes tablets and huge, unfolded, landscape-oriented foldable. They have a width of more than 840dp.
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)
add the library and sync.
implementation "androidx.compose.material3:material3-window-size-class:1.0.0-alpha14"
add the below code to your MainActivity.kt
val windowSizeClass = calculateWindowSizeClass(activity = this)
The code above returns the window size class for the provided activity. calculates WindowSizeClass
for the provided activity. The method returns a new WindowSizeClass
one during screen rotation or window resizing. The app recomposes the UI with the new window size class.
Add any missing imports by pressing Alt-Enter on the 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 supply the composable with windowSizeClass. Later, you’ll use this knowledge to choose the app layouts.
windowSizeClass = windowSizeClass.widthSizeClass,
You’ll also take device fold posture into account when updating the app to accommodate changes in screen sizes.
Observing the Device’s Folded Posture
A foldable gadget has a variety of states and positions. It may be folded or unfolded and can be oriented either in portrait or landscape. It can be holding a book or a tabletop. Various folding postures are supported by an adaptive design.
The WindowLayoutInfo class of the Jetpack WindowManager library offers the following details regarding foldable displays:
- state: The folded state is described by this. When the device is entirely opened, or HALF OPENED, its value is FLAT.
- orientation: The hinge’s orientation. It might be vertical or horizontal.
- occlusionType: When the hinge conceals a portion of the display, the value is FULL. If not, NONE is the value.
- isSeparating: When the hinge produces two logical displays, it is true.
The following postures are defined by DevicePosture
- Normal posture: Whether a device is fully folded or opened, a normal posture is either.
- Book posture: The device’s fold state is HALF OPENED and it is in portrait orientation.
- Separating posture: The gadget is in an open position with a FLAT fold condition. It is comparable to the situation where a physical hinge causes occlusion type to be FULL in a device’s position. Don’t put anything that can be touched or seen underneath the hinge.
Device Fold Posture Analysis
// 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
)
To stop Android Studio from complaining, you should also include the following imports:
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
Kotlin Flows are used in the code above to interact with WindowLayoutInfo data collection.
- windowLayoutInfo(activity: Activity) returns Flow, which is a device’s display information. Every time the display information is altered, the procedure produces WindowLayoutInfo.
- To establish the device fold posture, it employs the map operator and the display data supplied by windowLayoutInfo(activity: Activity).
You will then see the device’s posture and composition status. MainActivity.kt contains
val devicePosture = devicePostureFlow.collectAsState().value
Selecting the Correct Navigation Type
Different sorts of navigational elements that adjust to changes in display size are included in responsive user interfaces.
The material library offers navigational elements such as drawer, rail, and bottom navigation. Depending on the device’s window size class, you’ll implement the navigation that is most appropriate:
- Bottom navigation: Navigation at the bottom: For small window sizes, bottom navigation is ideal.
- Navigation rail: Use the navigation rail for medium-sized screens.
- Navigation drawer: This navigation drawer would work well with tablets and other large-screen devices. Navigation drawers come in two varieties: modal and permanent. Use a modal navigation drawer for compact to medium sizes because it can be enlarged as an overlay on the content or concealed. For fixed navigation on large screens, such as those found in tablets and Chrome OS devices, use a persistent navigation drawer.
Use 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
}
}
What the code above does is:
- declares the variable for navigationType.
- Depending on the window size class, it initializes navigationType with the appropriate value using a switch statement.
- handles the folded position to prevent inserting items or performing touching actions near the hinge. Use a navigation rail to split information around the hinge when a device is in BookPosture. Use a permanent navigation drawer for large desktops or tablets.
The navigationType argument is then passed to the NoteNavigationWrapperUi() composable method.
navigationType = navigationType,
Note: The application is now aware of the navigational approaches to use for various window size classes and device fold postures. To ensure optimal engagement and reachability, you’ll implement new navigation strategies next.
Putting in place Responsive Navigation
Add the following code to your main class.
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 additional imports you must include:
import kotlinx.coroutines.launch
import androidx.compose.material3.*
The notes UI is housed in the navigation drawer. Depending on the value of navigationType, the aforementioned code wraps the composable call with a permanent or modal navigation drawer.
Add the below code in your class
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)
}
}
}
Additionally, provide the following imports to please Android Studio:
The positioning of the bottom navigation or navigation rail is determined by navigationType in the code above. The AnimatedVisibility() composable was used to enclose both the bottom navigation and the navigation rail. Depending on the navigationType, this animates the entry and exit visibility of each navigation.
Build and run.