Jetpack Window Manager for foldable devices

Jetpack Window Manager provides a standard API for working with all foldable devices. It contains two important classes:

  • DisplayFeature - Identifies disruptions in the continuous flat screen surface such as hinges or folds. Window Manager will return a collection of display features from a layout change callback.
  • FoldingFeature - Provides information about a specific feature of the device. While the Surface Duo only has one folding feature, it's possible that other devices might have more.

A similar guide is in the Android Foldable Codelab. Read more about developing for foldables in the Android docs. Examples by the Android team are also available on GitHub. Jetpack release notes record changes in Window Manager as it is updated.

Tip

The controls and helper classes in the Surface Duo dual-screen library work with Window Manager. Follow the instructions to add the correct packages to your app project.

To use Window Manager directly in your code, follow the instructions below:

Add dependencies

  1. Make sure you have the mavenCentral() repository in your top-level build.gradle file:

    allprojects {
        repositories {
            google()
            mavenCentral()
         }
    }
    
  2. Ensure the compileSdkVersion and targetSdkVersion are set to API 31 or newer in your module-level build.gradle file:

    android { 
        compileSdkVersion 31
    
        defaultConfig { 
            targetSdkVersion 31
        } 
        ... 
    }
    
    
  3. Add the following dependencies to your module-level build.gradle file:

    dependencies {
        implementation "androidx.window:window:1.0.0"
        implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
    }
    

Use Window Manager in your Kotlin code

When accessing Window Manager properties in your Kotlin projects, it's important to set up the correct flow of information. Otherwise, you may receive too few or too many event updates, and app performance could be affected.

To initialize and use a WindowInfoTracker object, follow the steps below:

  1. In your MainActivity class, create a variable for the WindowInfoTracker. Ensure that import androidx.window.layout.WindowInfoTracker is added to the top of the file.

    class MainActivity : AppCompatActivity() {
        private lateinit var windowInfoTracker: WindowInfoTracker
    
  2. Initialize the WindowInfoTracker in your activity's onCreate method and set up a flow to collect information from the windowLayoutInfo property.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    
        // Initialize the window manager
        windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)
    
        // Set up a flow
        lifecycleScope.launch(Dispatchers.Main) {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                windowInfoTracker.windowLayoutInfo(this@MainActivity)
                    .collect { 
                        // Check FoldingFeature properties here
                    }
            }
        }
    }
    

    Ensure that these imports are also added to the top of the file:

    import androidx.lifecycle.Lifecycle
    import androidx.lifecycle.lifecycleScope
    import androidx.lifecycle.repeatOnLifecycle
    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.flow.collect
    import kotlinx.coroutines.launch
    
  3. Add code to check the WindowLayoutInfo flow for folding feature properties. When this code is run, the activity will update with the current device posture and display features (if spanned across a fold or a hinge).

    In the code snippet below, the activity displays different text based on the properties of a FoldingFeature.

    This example has a TextView called layout_change_text that shows the occlusion type and isSeparating value for any detected folding features.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    
        windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)
    
        lifecycleScope.launch(Dispatchers.Main) {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                windowInfoTracker.windowLayoutInfo(this@MainActivity)
                    .collect { newLayoutInfo ->
                        layout_change_text.text = "No display features detected"
                        for (displayFeature : DisplayFeature in newLayoutInfo.displayFeatures) {
                            if (displayFeature is FoldingFeature && displayFeature.occlusionType == FoldingFeature.OcclusionType.NONE) {
                                layout_change_text.text = "App is spanned across a fold, " +
                                    "isSeparating = ${displayFeature.isSeparating}"
                            }
                            if (displayFeature is FoldingFeature && displayFeature.occlusionType == FoldingFeature.OcclusionType.FULL) {
                                layout_change_text.text = "App is spanned across a hinge, " +
                                    "isSeparating = ${displayFeature.isSeparating}"
                            }
                        }
                    }
            }
        }
    }
    

Folding feature properties

The WindowLayoutInfo class has a collection of DisplayFeature items, one or more of which could be instances of the FoldingFeature class.

Folding features have the following properties:

  • bounds - coordinates of the bounding rectangle of a folding feature
  • occlusionType - if a folding feature hides content (FULL or NONE)
  • orientation - orientation of a folding feature (HORIZONTAL or VERTICAL)
  • state - angle of a folding feature (HALF_OPENED or FLAT)
  • isSeparating - if a folding feature separates the display area into two distinct sections

You can query these properties to make decisions about how to adjust your layout after configuration changes.

isSeparating

When deciding where to place controls or how many panes of content to show, use the isSeparating property. This field will make sure your app provides the best user experience on all foldable devices:

  • For dual-screen devices, this will always be true when an app is spanned across the hinge
  • For other foldable devices, this will only be true when the state is HALF_OPENED, such as when a device is in the tabletop posture

Use the isSeparating property to decide whether to adapt your app's UI layout for a foldable device, or use the default UI when there is no separation:

private fun updateCurrentLayout(newLayoutInfo: WindowLayoutInfo) {
   for (displayFeature in newLayoutInfo.displayFeatures) {
       val foldFeature = displayFeature as? FoldingFeature
       foldFeature?.let {
           if (it.isSeparating) {
               // The content is separated by the FoldingFeature.
               // Here is where you should adapt your UI.
           } else {
               // The content is not separated.
               // Users can see and interact with your UI properly.
           }
       }
   }
}

To see a more elaborate example of how to use this field, check out this isSeparating sample.

Google also provides documentation and samples related to this property as part of its large-screen and foldable guidance.

Samples

The surface-duo-jetpack-window-manager-samples GitHub repository contains a number of Kotlin samples demonstrating different dual-screen user experience patterns built using Jetpack Window Manager and the traditional view system.

The surface-duo-compose-samples GitHub repository also has dual-screen Kotlin samples that use Jetpack Window Manager, but in these samples the UI is built with Jetpack Compose.

Java API

Refer to the Jetpack Window Manager with Java blog post and this Java sample to see how to access the WindowInfoTracker class via the WindowInfoTrackerCallbackAdapter.

Resources