Welcome to the seventh part of Understanding foldable devices. It's been almost six months since the previous instalment, Foldable-aware app layout, went live. Quite a few things have happened. First, the Google Pixel Fold is here. Although it comes with a hefty price tag, it will hopefully drive interest in this still new device category. Second, Google's most important apps are starting to look good on large screens. Which brings us to... third, there's also the Pixel Tablet, so Google is finally back in the tablet game.
There's also a not so nice thing: it now is pretty clear that Microsoft lost interest in its Surface Duo and Surface Duo 2 foldables. But often, when vendors fail, the community jumps in. Here, this is the case, too. Thai Nguyen, a software engineer who in the past has worked at Microsoft, released a couple of Android 13 builds for the Surface Duo and the Duo 2. The builds are based on PixelExperience, an AOSP based ROM with Google apps and all Pixel goodies included. What's important, Thai makes use of the foldable features of the Duos in a way really close to the Pixel Fold. If you have a Surface Duo or Duo 2, have a look at the corresponding XDA thread to get started.
compose_adaptive_scaffold
To make writing apps that look great on foldables and large screens as easy as possible, I started an open source library called compose_adaptive_scaffold. You can find it in the Google Dev Library and on GitHub.
compose_adaptive_scaffold is based on the idea of two panes, called body and secondary body. For small screens you pass alternatives (or variations) called small body and small secondary body (the latter one is optional). Depending on your screen layout, the pairs body and small body, and secondary body and small secondary body may even be the same. Two panes are the basis for Canonical Layouts, an important Material Design concept. Before I show you how easy it is to create the panes, I'd like to mention that compose_adaptive_scaffold is inspired by the Flutter package flutter_adaptive_scaffold.
To use compose_adaptive_scaffold, you just need to add it as an implementation dependency:
dependencies {
implementation "com.github.tkuenneth:compose_adaptive_scaffold:0.2.1"
}
Next, let's look at the activity.
class AdaptiveScaffoldDemoActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
setContent {
…
MaterialTheme(
content = {
…
},
colorScheme = defaultColorScheme()
)
}
}
}
}
}
launch
and repeatOnLifecycle
are needed to get the underlying machinery (Jetpack WindowManager) going. A future version of my library might even do this for you. defaultColorScheme()
is a nice little helper that gives your app Dark mode support and dynamic colours on supported systems.
Here's how content
is defined:
content = {
AdaptiveScaffold(
useDrawer = true,
startDestination = destination1,
otherDestinations = listOf(destination2),
onDestinationChanged = {
// do something
},
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(
id = R.string.app_name
)
)
})
},
)
},
All magic is handled by a composable called AdaptiveScaffold()
. Besides panes, it is based upon destinations. You pass a start destination, as well as a list of other destinations. Depending on the horizontal Window Size Class, compose_adaptive_scaffold uses a bottom navigation, a navigation rail, or a navigation drawer.
The following screenshot was taken on a simulated flip phone in portrait mode.
Here, the phone was rotated to landscape mode:
To understand why the colours have changed, let's look at the definition of destination1
.
val destination1 = NavigationDestination(
icon = R.drawable.ic_android_black_24dp,
label = R.string.app_name,
body = {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Red)
)
},
secondaryBody = {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Green)
)
},
smallBody = {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Blue)
)
},
smallSecondaryBody = {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Yellow)
)
},
)
So, a NavigationDestination
gets an icon, a label, and two pairs describing the panes:
- body and secondary body
- small body and small secondary body
Spanning panes
A destination may also receive an overlay.
val destination2 = NavigationDestination(
icon = R.drawable.ic_android_black_24dp,
label = R.string.app_name,
overlay = {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.LightGray)
)
},
)
Here we don't provide any pane. What's that useful for?
An overlay spans the two panes, it is laid out on top of them. In my example the panes are empty and the overlay uses all available space, but you can also make the overlay smaller, so it floats above the panes.
What's next?
compose_adaptive_scaffold is still in its infancy. I need to explore how to use it with canonical layouts. Also, combining the library with existing navigation frameworks is on the to do list. What else am I missing? And how do you like the general idea? Please share your thoughts in the comments.
Top comments (8)
Great post!
Reading about the library, i like to know:
How to handle that, in case of the folded device, the bottom navigation appear with adjusted width of the viewport? i mean, when the foldable device if separating, the bottom navigation bar (which sould appear when the first part of the divided screen is compact) is displaying in the left part of the separating screen, ah, in this example, im using a single scaffold composable
Thank you very much again for such good content about foldables on Android and I look forward to any reviews.
Hey Marlon. Glad you like my posts, thanks a lot. I'm not sure if I understand correctly. Could you maybe share a screenshot? Would love to discuss this further. Thank you
Hello,
I managed to hand draw this design of what the UI should look like for the scenario I described before.
The bottom navigation part is seen on the left.
I hope this drawing is of great help and explanation.
Regards,
Hi Marlon. Thanks for the drawing. Now I get it. At the moment, this unfortunately is not possible with compose_adaptive_scaffold. It follows a "both panes share one bottom navigation OR navigation rail" approach, depending on the window size class. What you could do, though, is use the two panes that compose_adaptive_scaffold provides, but implement the bottom navigation on your own. Your left pane would likely be a Column() with two children, your content and below that the bottom navigation. But you should even be able to use a Scaffold() instead.
Nice! I haven't seen that many people talk about this subject. I'll play with some of this stuff on my Samsung Z Flip4 :P
great!! is there any info about building an android enumator for the samsung galaxy z flip 4? i know about the skin for download, but, it isnt the same
Unfortunately, I haven't heard of one, sorry
Sounds cool. Thanks for trying out the library. If you have questions, feel free to reach out