Large Screens & Foldables Tutorial for Android
Learn how to build great user experiences for large screens & foldables in Android. Also learn how to design and test adaptive Android apps. By Beatrice Kinya.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Large Screens & Foldables Tutorial for Android
25 mins
- Getting Started
- Looking Into Android 12L Updates
- Designing Adaptive Apps
- Exploring Window Size Classes
- Looking Into Device Fold Posture
- Analyzing Device Fold Posture
- Choosing Appropriate Navigation Type
- Implementing Responsive Navigation
- Displaying More Content
- Ensuring Data is Available for All Screen Sizes
- Testing Apps for Large Screens With Android Studio
- Looking Into What to Test in Large Screens
- Checking Into Google Play Updates for Large Screens
- Where to Go From Here?
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, orHALF_OPENED
. -
orientation: The orientation of the hinge. It can be
HORIZONTAL
orVERTICAL
. -
occlusionType: The value is
FULL
when the hinge hides part of the display. Otherwise the value isNONE
. -
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 whereocclusionType
isFULL
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.
-
windowLayoutInfo(activity: Activity)
returns display information of a device asFlow
. The method emitsWindowLayoutInfo
every time the display information changes. - It uses
map
operator and display information returned bywindowLayoutInfo(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:
- Declares the
navigationType
variable. - Using a switch statement, it initializes
navigationType
with the correct value depending on the window size class. - 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:
In a medium window size class, the app uses a navigation rail like in the screen below:
The app uses a permanent navigation drawer in an expanded window size class, like this:
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.