DEV Community

Cover image for Re-gaining orientation #3

Re-gaining orientation #3

tkuenneth profile image Thomas Künneth ・4 min read

Welcome to the third part of my small series about screen orientation in Jetpack Compose. So far we have an app that in portrait mode features a traditional master-detail display where either master (for example a list of modules) or detail (the contents of one module) cover the whole screen. In landscape mode the screen is divided into two parts.

Two-column-view in landscape mode

The width of the list is smaller than the module. This makes sense because

  • the details should get as much screen real estate as possible
  • the list itself has not that much to show, because its texts are usually just parts of a sentence

At least on smartphones and traditional tablets.

Now let's see how the app looks like on an opened foldable device.

Running on an opened foldable device

Well, that's not what we want, right?


Please remember, currently the decision whether to show portrait or landscape mode looks like this:

isTwoColumnMode = (LocalConfiguration.current.orientation
    == Configuration.ORIENTATION_LANDSCAPE)
if (isTwoColumnMode)
Enter fullscreen mode Exit fullscreen mode

Although the foldable device is open, it still is held in portrait mode.

One way to make the decision smarter is to look at the available screen width. So we might argue that if there are at least 600 density-independent pixels this is enough to show two columns. That's how alternative layout resources have been working for quite a long time.

isTwoColumnMode = LocalConfiguration.current.screenWidthDp >= 600
Enter fullscreen mode Exit fullscreen mode

Emulator showing a completely open foldable

But what if the foldable has two screens with a noticeable gap between them?

The Surface Duo Emulator

Depending on the content an app wants to present it may make sense to treat the screen real estate as two equally sized blocks. Please recall how my example organizes its composables.

fun Landscape(module: MutableState<Module?>) {
  Row(modifier = Modifier.fillMaxSize()) {
      module = module,
      modifier = Modifier.weight(weight = 0.3f)
    module.value?.let {
        module = it,
        modifier = Modifier.weight(0.7f)
Enter fullscreen mode Exit fullscreen mode

As you can see, Modifier.weight() is passed 0.3f for the module selection and 0.7f for the module. Instead of hardcoding the values we can easily pass them as parameters.

fun Landscape(
  module: MutableState<Module?>,
  weightModuleSelection: Float,
  weightModule: Float
) {
  Row(modifier = Modifier.fillMaxSize()) {
      module = module,
      modifier = Modifier.weight(weight = weightModuleSelection)
    module.value?.let {
        module = it,
        modifier = Modifier.weight(weightModule)
Enter fullscreen mode Exit fullscreen mode

But how do we determine the ratio? Or, to put it another way, when do we pass 0.5F twice?

The Jetpack Window Manager component aims to

help application developers support new device form factors
and provide a common API surface for different
Window Manager features on both old and new platform

To use it we need to add the following line to our dependencies section in the build.gradle file of the app module:

implementation "androidx.window:window:1.0.0-alpha05"
Enter fullscreen mode Exit fullscreen mode

androidx.window.WindowManager is the main interaction point with the library.

private var weightModuleSelection by mutableStateOf(0.3F)
private var weightModule by mutableStateOf(0.7F)

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    val wm = WindowManager(this)
    wm.registerLayoutChangeCallback(mainExecutor, {
      it.displayFeatures.forEach { feature ->
        (feature as FoldingFeature).run {
          if (isSeparating) {
            weightModuleSelection = 0.5F
            weightModule = 0.5F
    setContent {
Enter fullscreen mode Exit fullscreen mode

After obtaining an instance of WindowManager we register a callback for layout changes of the window of the current visual Context. The callback receives WindowLayoutInfo objects, which in turn contain a list of DisplayFeatures. According to the doc, a...

display feature is a distinctive physical attribute located
within the display panel of the device. It can intrude into
the application window space and create a visual
distortion, visual or touch discontinuity, make some area
invisible or create a logical divider or separation in the
screen space.

FoldingFeature implements this interface. It

describes a fold in the flexible display or a hinge
between two physical display panels.

isSeparating returns true if...

a FoldingFeature should be thought of as splitting the
window into multiple physical areas that can be seen by
users as logically separate. Display panels connected by
a hinge are always separated. Folds on flexible screens
should be treated as separating when they are not

On the Surface Duo Emulator the example looks as follows:

Updated code on the Surface Duo Emulator

On the stock Android Emulator simulating a foldable the app looks like this:

Updated code on the stock Android Emulator simulating a foldable

While both results look quite pretty we have a few thing to do. Can you guess what? Please stay tuned for a follow-up.


Discussion (0)

Editor guide