Make your app fold aware

Working with foldables offers a lot of opportunities for innovation, either by taking advantage of additional screen real estate provided by large unfolded displays or by creating new experiences based on folded states. To make your app fold aware, use Jetpack WindowManager, a library that provides a common API surface for different window features, like folds or a hinge. When your app is fold aware, the content in the window can be adapted to avoid folds and hinges or to take advantage of them and use them as natural separators.

Window information

WindowManager provides information about the window via WindowInfoTracker. This interface exposes window layout information via the WindowInfoTracker.windowLayoutInfo method as a stream of events that you can react to. For example, an application can receive events when there’s a change in the device’s fold state, collecting WindowLayoutInfo objects and updating the app layout while the application is visible to the user. The entry point for this is the WindowInfoTracker.getOrCreate(Context) function that provides an instance of WindowInfoTracker that is associated to the given Context.

WindowManager provides support for Kotlin Flows and callbacks.

Kotlin with Flows

To start and stop the event collection, we can use a restartable lifecycle-aware coroutine. Here, the block passed to repeatOnLifecycle is executed when the lifecycle is at least STARTED and is canceled when the lifecycle is STOPPED. It then automatically restarts the block when the lifecycle is STARTED again. In this way it can safely collect from windowLayoutInfo(Activity), using the context of the current activity as a parameter, when the lifecycle is STARTED and stop collection when it is STOPPED:

class DisplayFeaturesActivity : AppCompatActivity() {

    private lateinit var binding: ActivityDisplayFeaturesBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
        setContentView(binding.root)

        lifecycleScope.launch(Dispatchers.Main) {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
                    .windowLayoutInfo(this@DisplayFeaturesActivity)
                    .collect { newLayoutInfo ->
                        // Use newLayoutInfo to update the layout.
                    }
            }
        }
    }
}

Java with callbacks

To collect WindowLayoutInfo updates without using a Kotlin Flow, use the callback compatibility layer included in the androidx.window:window-java dependency. This artifact provides the WindowInfoTrackerCallbackAdapter that, starting from a WindowInfoTracker, enables you to register (and unregister) a callback to receive WindowInfoLayout updates.

public class SplitLayoutActivity extends AppCompatActivity {

    private WindowInfoTrackerCallbackAdapter windowInfoTracker;
    private ActivitySplitLayoutBinding binding;
    private final LayoutStateChangeCallback layoutStateChangeCallback =
            new LayoutStateChangeCallback();

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

       windowInfoTracker =
                new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
   }

   @Override
   protected void onStart() {
       super.onStart();
       windowInfoTracker.addWindowLayoutInfoListener(
                this, Runnable::run, layoutStateChangeCallback);
   }

   @Override
   protected void onStop() {
       super.onStop();
       windowInfoRepository
           .removeWindowLayoutInfoListener(layoutStateChangeCallback);
   }

   class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
       @Override
       public void accept(WindowLayoutInfo newLayoutInfo) {
           this@SplitLayoutActivity.runOnUiThread {
           // Use newLayoutInfo to update the Layout
           }
       }
   }
}

RxJava support

If you are already using RxJava (version 2 or 3), you can use specific artifacts that allow you to use Observable or Flowable to collect WindowLayoutInfo updates without using a Kotlin Flow.

Use the compatibility layer included in the androidx.window:window-rxjava2 or the androidx.window:window-rxjava3 dependency. This artifact provides WindowInfoTracker#windowLayoutInfoFlowable(Activity) and WindowInfoTracker#windowLayoutInfoObservable(Activity) that, starting from a WindowInfoTracker, enables you to receive WindowInfoLayout updates.

class RxActivity: AppCompatActivity {

    private lateinit var binding: ActivityRxBinding

    private var disposable: Disposable? = null
    private lateinit var observable: Observable<WindowLayoutInfo>

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

        // Create a new observable
        observable = WindowInfoTracker.getOrCreate(this@RxActivity)
            .windowLayoutInfoObservable(this@RxActivity)
   }

   @Override
   protected void onStart() {
       super.onStart();

        // Subscribe to receive WindowLayoutInfo updates
        disposable?.dispose()
        disposable = observable
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { newLayoutInfo ->
            // Use newLayoutInfo to update the Layout
            // TODO
        }
   }

   @Override
   protected void onStop() {
       super.onStop();

        // Dispose the WindowLayoutInfo observable
        disposable?.dispose()
   }
}

Foldables and display features

Jetpack WindowManager is built to support different foldable form factors, and its design is intended to also support devices that will be released in the future. To support future compatibility, display information is delivered as part of WindowLayoutInfo in a list of DisplayFeature elements. This basic interface describes the physical features of a display. For example, DisplayFeature.bounds can define the bounds of the fold of a foldable device.

Folding features

A FoldingFeature is a type of DisplayFeature that provides information related to the fold of a foldable display or the hinge between the two physical display panels of a dual-screen device. In particular, FoldingFeature provides the state and orientation of a foldable device:

We can use these values to check whether the device is in tabletop or book mode:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
                .windowLayoutInfo(this@DisplayFeaturesActivity)
                .collect { newLayoutInfo ->
                    // Use newLayoutInfo to update the Layout
                    val foldingFeature: FoldingFeature? =
                        newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature
                    val isTableTop = isTableTopMode(foldingFeature)
                }
        }
    }
}

private fun isTableTopMode(foldFeature: FoldingFeature?) =
    foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
        foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL

private fun isBookMode(foldFeature: FoldingFeature?) =
    foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
        foldFeature.orientation == FoldingFeature.Orientation.VERTICAL

Java

    private WindowInfoTrackerCallbackAdapter windowInfoTracker;
    private final LayoutStateChangeCallback layoutStateChangeCallback =
            new LayoutStateChangeCallback();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        ...

        windowInfoTracker =
                new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
    }

    @Override
    protected void onStart() {
        super.onStart();
        windowInfoTracker.addWindowLayoutInfoListener(this, Runnable::run, layoutStateChangeCallback);
    }

    @Override
    protected void onStop() {
        super.onStop();
        windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback);
    }


class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
    @Override
    public void accept(WindowLayoutInfo newLayoutInfo) {
        // Use newLayoutInfo to update the Layout
        List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures();
        for (DisplayFeature feature : displayFeatures) {
            if (feature instanceof FoldingFeature) {
                boolean isTableTop = isTableTopMode((FoldingFeature) feature);
            }
        }
    }
}


private boolean isTableTopMode(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
            (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
            (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}

private boolean isBookMode(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
            (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
            (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}

A FoldingFeature also includes information like occlusionType, which indicates whether the fold or hinge conceals part of the display, and isSeparating, which indicates whether the folding feature creates two logical screen areas. This information can be used to decide where to position on screen elements to support dual screen devices and avoid positioning active elements like buttons on occluded folding features.

Window size changes

Since screen size can vary when there’s a configuration change, it’s important to start designing fully responsive and adaptive UIs. Another feature included in the WindowManager library is the ability to retrieve the current and maximum window metrics information. This is similar to the information provided by the platform WindowMetrics API included in API level 30, but it is backward compatible down to API level 14.

Jetpack WindowManager enables you to retrieve WindowMetrics information through the WindowMetricsCalculator class.

Use WindowMetricsCalculator in an activity’s onCreate() method to configure your views, in onConfigurationChanged() when handling configuration changes, or in tests (see Test your apps on foldables):

Kotlin

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    val windowMetrics = WindowMetricsCalculator.getOrCreate()
        .computeCurrentWindowMetrics(this)
}

Java

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    WindowMetrics windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this);
}