Foldable-aware app layout
Tags: android, foldable, jetpackcompose
Published: 2023-02-16 10:35:03

Welcome to the final part of *Understanding foldable devices*. As this series has been going on for some time, let's start with a short recap. 

In the first three installments, I showed you why foldables may or may not be large screen devices, how folds and hinges influence the layout of your UI elements, and which tools you can use to test your apps' behavior on different device classes and form factors. 

The forth part provided all code you need to make your Compose app behave well on smartphones, tablets, foldables, and Freeform experiences. An important part of my sample utilizes *Jetpack WindowManager* to gather information about the screen, the app window, and - if present - the fold or hinge. One bit of information is called *Window size class*. I used it to define the inner app layout. As there are actually two Window size class implementations, the previous (fifth) installment reflected on them.

While my sample code did provide a basic app frame by using `Scaffold()` and `TopAppBar()`, it lacked one important aspect: **navigation**. In this sixth part, we will look at corresponding composables. As you will see shortly, the device category influences which one you should use. I'll also show you, that the different navigation composables affect the amount of space available for the content, as well as its location on screen.

Sounds interesting? Great, kindly read along.

### Layout anatomy

The most important part of your app is its **content**. The content is the reason why your users run the app. Please recall that, depending on the device category and available space, I suggest one, two, or maybe even more columns. But what's inside these columns? We'll turn to this in section *Content*. The Material Design documentation, by the way, refers to the content as **body**. But I feel *content* is more expressive. 

Another important area on screen is reserved for navigation. Looking at common Android apps, you'll spot a few places where navigation can happen:

- at the top of the app window (`TopAppBar()`, …)
- at the bottom of the app window (`NavigationBar()`, …)
- at the left app window border (`PermanentNavigationDrawer()`, `NavigationRail()`, …)

In case you are wondering why I use the term *app window* instead of *screen*… While the space that is available to an app is limited by screen sizes, this does **not** mean than the app can fully occupy this area. On Freeform experiences we have resizable windows, and even on a smartphone we can run two apps side by side in split screen mode. So, the area *on screen* that is available to an app can change over time.

While app bars display information and actions, these actions may well trigger navigation, too. Therefore, I treat app bars as navigation components. 

Here are some takeaways:

1. There are two main layout regions, **Navigation** and **Content**.
2. We have several navigation composables to choose from. 
3. The term *layout* may refer to the *outer layout* (location and size of navigation components and the content) and the *inner layout* (how the content is presented).

Parts of the outer layout are handled by provided composables (for example, `Scaffold()`). But some need to be managed by your app. Here's how the latest version of my *FoldableDemo* sample looks like in different situations:

![FoldableDemo running in portrait smartphone mode](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f62ubs80baxr64iqmf6y.jpg)

On smartphones in portrait mode we typically have app bars at the top and bottom. The top app bar may also include a modal navigation drawer (to keep things simple, *FoldableDemo* doesn't have one).

![FoldableDemo running in landscape smartphone mode](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pn2v895ddie1gfi6by4h.jpg)

If a device is wide enough (you'll see shortly how this is calculated), the app may switch to a navigation rail. On even larger screens this might also be a permanent navigation drawer (to keep things simple, *FoldableDemo* doesn't do so).

![FoldableDemo running in portrait foldable mode](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m8renugxby6vo6eartwo.jpg)

On an open foldable device in portrait mode, we will likely use a navigation rail or a permanent navigation drawer. The app uses a two column layout for the content. Please note that, because of the navigation rail, the left column is slightly smaller.

![FoldableDemo running in landscape foldable mode](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tsn32kowxo9k168gvqzf.jpg)

If we rotate the foldable (the fold or hinge is running horizontally), I suggest using one column but two rows for the content, particularly if the hinge is obstructing. As you know from the previous parts, my sample code takes obstructing folds and hinges into account, therefore no part of your content becomes invisible. But how do you use the two columns or rows? We'll tackle this in section *Content*.

Now, you may be thinking *Well, that looks great, but how does the revised code look like?*

**Window size classes** are a key element in defining the app layout. The previous version of *FoldableDemo* has already used them for the inner app layout, to differentiate between smartphone, foldables, large screen devices and Freeform experiences. It still does, but now the sample also uses these *switches* to toggle between …

- Bottom bar navigation and navigation rail or navigation drawer
- displaying a top app bar and not displaying a top app bar

Take a look:

```kotlin
setContent {
  val layoutInfo by WindowInfoTracker.getOrCreate(this@FoldableDemoActivity)
    .windowLayoutInfo(this@FoldableDemoActivity).collectAsState(
      initial = null
    )
  val windowMetrics = WindowMetricsCalculator.getOrCreate()
    .computeCurrentWindowMetrics(this@FoldableDemoActivity)
  // might become part of some UIState - kept here for simplicity
  val foldDef = createFoldDef(layoutInfo, windowMetrics)
  val hasTopBar =
    foldDef.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
  val hasBottomBar =
    foldDef.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
  val hasNavigationRail = !hasBottomBar
  val index = rememberSaveable { mutableStateOf(0) }
  MaterialTheme(
    content = {
      Scaffold(
        topBar = { MyTopBar(hasTopBar = hasTopBar) },
        bottomBar = {
          MyBottomBar(
            hasBottomBar = hasBottomBar,
            index = index
          )
        }
      ) { padding ->
        Content(
          foldDef = foldDef,
          paddingValues = padding,
          hasNavigationRail = hasNavigationRail,
          index = index
        )
      }
    },
    colorScheme = defaultColorScheme()
  )
}
```

Based on `foldDef`, we define a few variables, `hasTopBar`, `hasBottomBar`, and `hasNavigationRail`, and pass them to several composables. Here's what `MyBottomBar()` does:

```kotlin
@Composable
fun MyBottomBar(hasBottomBar: Boolean, index: MutableState<Int>) {
  if (hasBottomBar)
    NavigationBar {
      for (i in 0..2)
        NavigationBarItem(selected = i == index.value,
                 onClick = { index.value = i },
                 icon = {
                   Icon(
                     painter = painterResource(id = R.drawable.ic_android_black_24dp),
                     contentDescription = null
                   )
                 },
                 label = {
                   MyText(index = i)
                 }
        )
    }
}
```

So, `MyBottomBar()` invokes `NavigationBar {}` only if `hasBottomBar` is `true`. Do you remember I said that Window size classes are used like switches? Take a look:

```kotlin
val hasBottomBar =
    foldDef.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
```

In your app you may want to put those switches in some UI state class and pass that state to your composables. I decided to keep the bare variables to make things more transparent and simple.

### Content

Here's how the updated `Content()` composable looks like:

```kotlin
@Composable
fun Content(
  foldDef: FoldDef,
  paddingValues: PaddingValues,
  hasNavigationRail: Boolean,
  index: MutableState<Int>
) {
  Row(modifier = Modifier.fillMaxSize()) {
    if (hasNavigationRail)
      NavigationRail {
        for (i in 0..2)
          NavigationRailItem(selected = i == index.value,
                    onClick = {
                      index.value = i
                    },
                    icon = {
                      Icon(
                        painter = painterResource(id = R.drawable.ic_android_black_24dp),
                        contentDescription = null
                      )
                    },
                    label = {
                      MyText(index = i)
                    })
      }
    BoxWithConstraints(
      modifier = Modifier
        .fillMaxSize()
        .padding(paddingValues = paddingValues)
    ) {
      if (foldDef.hasFold) {
        FoldableScreen(
          foldDef = foldDef
        )
      } else if (foldDef.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) {
        LargeScreen(
          foldDef = foldDef
        )
      } else {
        SmartphoneScreen(
          foldDef = foldDef
        )
      }
    }
  }
}
```

Have you noticed, that, besides the name of the composable, my `NavigationRailItem` and `NavigationBarItem` creation loops are the same? I really wish, Google would add a common composable, `NavigationItem`.  This would greatly ease reuse and avoid code duplication. Anyway. While the `BoxWithConstraints()` was already present in the previous version, it is now a child of `Row()`. There's also a `NavigationRail ()` if `hasNavigationRail` is `true`. So, depending on that switch, `Row()` has either one or two children. That's, by the way, the reason I said parts of the *outer layout* need to be handled by you. To make your app layout even more sophisticated, you can show a permanent navigation drawer instead of the navigation rail on large screens. To do this, you first need to define your switches like this:

```kotlin
val hasNavigationRail =
      foldDef.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM
val hasPermanentNavigationDrawer =
      foldDef.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED
```

Then, inside `Content()`, you just invoke the corresponding composables based on that switches (either `NavigationRail()` or `PermanentNavigationDrawer()`.

While I was adding support for navigation rails, I spotted a small glitch in my previous implementation. Can you guess what happened here?

![Screenshot of a source code diff](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/inmffv1h7f9750bi6hzd.png)

You may remember that, at least on devices with a fold or hinge, I suggest a two column layout with equally sized areas. However, the navigation rail shrinks the space being available for content in the left column. We need to take this into account. While we could try to calculate the width of the navigation rail and subtract it, it is easier to let Jetpack Compose do the work.

### Final Remarks

*FoldableDemo* uses colored boxes to represent the app content. Based on the device category and form factor, the content area may be divided into two ore more columns. Please recall that my sample code handles folds and hinges for you, so you just need to *fill* the columns. With what, depends on the the purpose of your app. Google has looked at lots of apps and has identified three general layout types:

- **Feed** arranges cards or other content elements in a grid
- **List-detail** displays item lists and the detail of one (selected, current) item side by side
- **Supporting pane** organizes app content into primary and secondary display areas

Google calls them [Canonical layouts](https://m3.material.io/foundations/layout/canonical-layouts/overview), which are described as

> Ready-to-use compositions that help layouts adapt for common use cases and screen sizes.

The idea is to use them as starting points for organizing common elements in your app. The Material Design website explains:

> Each layout considers common use cases and components to address expectations and user needs for how apps adapt across window class sizes and breakpoints.

So, at least for now, canonical layouts are more like concepts than ready-to-use components. Yet, Google offers quite a few samples that make use of / implement them. Please refer to the *Resources* section for details. I may be providing an enhanced version of *FoldableDemo* later this year. 

Well, that's it. I hope that you enjoyed this series. Please share your thoughts in the comments.

---

Unless specified otherwise, all pictures are (c) Thomas Künneth

#### Resources

- The [FoldableDemo](https://github.com/tkuenneth/android-demos/tree/master/compose/FoldableDemo) sample on GitHub
- [Google - Build adaptive layouts](https://developer.android.com/jetpack/compose/layouts/adaptive) 
- [Google GitHub repo - Large screen canonical layouts](https://github.com/android/user-interface-samples/tree/main/CanonicalLayouts)