Don't Lock the Screen Orientation! Handling Orientation in Compose

Eevis - Jul 18 - - Dev Community

Yes, your app should work in both portrait and landscape modes - unless locked orientation is essential for functionality, like in a piano app. However, for most apps, locked orientation is not an option. Oh, and yes, the functionality should be similar in both modes, not hiding anything when the orientation changes.

In this blog post, I'll cover the topic of locked orientation. It's mainly concentrated on Compose things, but there might be something useful for Views, too. I've also included some good articles in the Read More section of the blog post, explaining, e.g., more about orientation locking themes on Views.

This blogpost was updated on 22nd of July to add example of using WindowWidthSizeClass.

Why Locking Orientation is a Problem?

When discussing locked orientation and its problems, we usually refer to locking the orientation to portrait mode. Unfortunately, this is a typical case among Android apps.

Why is this a problem, then? Some people prefer to use their phones in landscape mode because it gives them more screen width. A wider screen can make it easier for people who use, e.g., larger font sizes of screen magnification. And some people might have their phones mounted on, e.g., a wheelchair in landscape mode, and can't rotate the phone at all.

In these cases, if the app's orientation is locked in portrait mode, using the app might be more complicated or even impossible. That is not what any developer behind the apps wants, right? So, let's talk about how to support different orientations on your app.

It's Straightforward

From a purely UI point of view, supporting landscape mode is pretty straightforward. You just don't do anything - like prevent the screen rotation. This way, the landscape mode is available.

Of course, you need to test that the layouts actually look okay in landscape orientation. With Compose, the layout mostly works if you don't use exact widths for different elements and don't have sticky items that take up some vertical space on the screen. However, in case you notice some problems, I've described one way to handle different layouts for landscape and portrait modes later in this post.

But You Need to Remember Configuration Changes

But there is one thing: You need to remember configuration changes. Switching the screen orientation is a configuration change, which can then trigger all kinds of things.

Android developer documentation has an extensive page on reacting to configuration changes and the logic behind them: Handle Configuration Changes. In the following subsections, I'm going to concentrate on Jetpack Compose-related things. We'll first discuss UI-related changes and then data-related changes.

Handling UI Changes

Although I mentioned that the UI usually looks pretty okay in landscape mode if there are no sticky elements or fixed widths for elements, the experience could often be better.

Let's say we have a button with a short text like "Save" that takes the full width in portrait mode. In landscape mode, we would like to keep the button's width at 75% of the screen width. We have two ways to do this: Orientation and WindowWidthSizeClass. I'll first give an example with orientation, then discuss WindowWidthSizeClass.

Orientation

One way to accomplish this is to get the current orientation and then act on its value. We can get it from LocalConfiguration composition local. Let's look at an example:

@Composable
fun OrientationExample() { 
    val orientation = LocalConfiguration.current.orientation
    when (orientation) {
        Configuration.ORIENTATION_LANDSCAPE -> {
            // Version for Landscape orientation
        }
        else -> {
            // Version for Portrait orientation
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, we first read the orientation from LocalConfiguration.current and then do something based on its value. So, for the example of our button, we could use this to save the value modifier uses for the width:

val buttonWidth = when (orientation) {
    Configuration.ORIENTATION_LANDSCAPE -> 0.75f
    else -> 1f
}
Enter fullscreen mode Exit fullscreen mode

And then use it in the code:

Button(
    modifier = Modifier.fillMaxWidth(buttonWidth)
    ...
)
Enter fullscreen mode Exit fullscreen mode

This way, our UI can react to screen orientation changes to provide a better user experience.

WindowWidthSizeClass

There's another, a bit more flexible way to handle different screen widths as well: WindowWidthSizeClass. It's a class that represents breakpoints and helps to build flexible layouts. As the documentation describes:

 Each window size class breakpoint represents a majority case for typical device scenarios so your layouts will work well on most devices and configurations.

Appt.org gives an example of handling screen orientation in Jetpack Compose with WindowWidthSizeClass:

// Appt.org's Jetpack Compose code example

@Composable
fun WindowSizeExample(widthSizeClass: WindowWidthSizeClass) {
    when(widthSizeClass) {
        WindowWidthSizeClass.Expanded -> 
            // orientation is landscape in most devices including foldables (width 840dp+)
        WindowWidthSizeClass.Medium -> 
            // Most tablets are in landscape, larger unfolded inner displays in portrait (width 600dp+)
        WindowWidthSizeClass.Compact -> 
            // Most phones in portrait
    }
}
Enter fullscreen mode Exit fullscreen mode

This way, the UI would be even more flexible, handling landscape and portrait modes and different screen configurations, such as foldable phones.

Handling Data with Configuration Changes

The other issue with non-locked screen orientation is data loss. If we store something on a composable, it will be recreated on each recomposition unless we wrap it to remember, which saves things over recompositions. remember, however, doesn't store its value through activity recreation, which happens with configuration changes.

If you need to store something inside a composable through configuration changes, you can use rememberSaveable. It works well with primitive data types, but if you need to store some more complex data, you'll need to utilize different state-saving methods listed in the Android documentation: State and Jetpack Compose - Ways to store state.

There's one more thing I want to talk about: Business logic. If you're storing your data to a ViewModel, orientation changes are handled on that side automatically. However, note that ViewModels don't survive system-initiated process deaths. For that, Android documentation suggests using SavedState APIs, and if you want to read more about that, the information is in Save UI state in Compose - SavedStateHandle APIs.

Wrapping Up

In this blog post, we've discussed screen orientation changes, how a non-locked screen orientation is an accessibility requirement, and what aspects you should consider when developing applications related to screen orientation.

What's your experience with locked screen orientations, either as a user or as a developer?

Read More And Links in the Blog Post

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .