Large unfolded displays and unique folded states enable new user experiences on foldable devices. To make your app fold aware, use the Jetpack WindowManager library, which provides an API surface for foldable device window features such as folds and hinges. When your app is fold aware, it can adapt its layout to avoid placing important content in the area of folds or hinges and use folds and hinges as natural separators.
Window information
The WindowInfoTracker
interface in Jetpack WindowManager exposes window layout information. The interface's windowLayoutInfo()
method returns a stream of WindowLayoutInfo
data that informs your app about a foldable device’s fold state. The WindowInfoTracker
getOrCreate()
method creates an instance of WindowInfoTracker
.
WindowManager provides support for collecting WindowLayoutInfo
data using Kotlin Flows and Java callbacks.
Kotlin Flows
To start and stop WindowLayoutInfo
data collection, you can use a restartable lifecycle-aware coroutine in which the repeatOnLifecycle
code block is executed when the lifecycle is at least STARTED
and stopped when the lifecycle is STOPPED
. Execution of the code block is automatically restarted when the lifecycle is STARTED
again. In the following example, the code block collects and uses WindowLayoutInfo
data:
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 callbacks
The callback compatibility layer included in the androidx.window:window-java
dependency enables you to collect WindowLayoutInfo
updates without using a Kotlin Flow. The artifact includes the WindowInfoTrackerCallbackAdapter
class, which adapts a WindowInfoTracker
to support registering (and unregistering) callbacks to receive WindowLayoutInfo
updates, for example:
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();
windowInfoTracker
.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}
class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo newLayoutInfo) {
SplitLayoutActivity.this.runOnUiThread( () -> {
// Use newLayoutInfo to update the layout.
});
}
}
}
RxJava support
If you're already using RxJava
(version 2
or 3
), you can take advantage of artifacts that enable you to use an Observable
or Flowable
to collect WindowLayoutInfo
updates without using a Kotlin Flow.
The compatibility layer provided by the androidx.window:window-rxjava2
and androidx.window:window-rxjava3
dependencies includes the WindowInfoTracker#windowLayoutInfoFlowable()
and WindowInfoTracker#windowLayoutInfoObservable()
methods, which enable your app to receive WindowLayoutInfo
updates, for example:
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
}
}
@Override
protected void onStop() {
super.onStop();
// Dispose the WindowLayoutInfo observable
disposable?.dispose()
}
}
Features of foldable displays
The WindowLayoutInfo
class of Jetpack WindowManager makes the features of a display window available as a list of DisplayFeature
elements.
A FoldingFeature
is a type of DisplayFeature
that provides information about foldable displays, including the following:
state
: The folded state of the device,FLAT
orHALF_OPENED
orientation
: The orientation of the fold or hinge,HORIZONTAL
orVERTICAL
occlusionType
: Whether the fold or hinge conceals part of the display,NONE
orFULL
isSeparating
: Whether the fold or hinge creates two logical display areas, true or false
A foldable device that is HALF_OPENED
always reports isSeparating
as true because the screen is separated into two display areas. Also, isSeparating
is always true on a dual-screen device when the application spans both screens.
The FoldingFeature
bounds
property (inherited from DisplayFeature
) represents the bounding rectangle of a folding feature such as a fold or hinge. The bounds can be used to position elements on screen relative to the feature.
Use FoldingFeature
state
to determine whether the device is in tabletop or book posture, and customize your app layout accordingly, for example:
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { ... lifecycleScope.launch(Dispatchers.Main) { // The block passed to repeatOnLifecycle is executed when the lifecycle // is at least STARTED and is cancelled when the lifecycle is STOPPED. // It automatically restarts the block when the lifecycle is STARTED again. lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { // Safely collects from windowInfoRepo when the lifecycle is STARTED // and stops collection when the lifecycle is STOPPED WindowInfoTracker.getOrCreate(this@MainActivity) .windowLayoutInfo(this@MainActivity) .collect { layoutInfo -> // New posture information val foldingFeature = layoutInfo.displayFeatures .filterIsInstance() .firstOrNull() when { isTableTopPosture(foldingFeature) -> enterTabletopMode(foldingFeature) isBookPosture(foldingFeature) -> enterBookMode(foldingFeature) isSeparating(foldingFeature) -> // Dual-screen device if (foldingFeature.orientation == HORIZONTAL) { enterTabletopMode(foldingFeature) } else { enterBookMode(foldingFeature) } else -> enterNormalMode() } } } } } @OptIn(ExperimentalContracts::class) fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean { contract { returns(true) implies (foldFeature != null) } return foldFeature?.state == FoldingFeature.State.HALF_OPENED && foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL } @OptIn(ExperimentalContracts::class) fun isBookPosture(foldFeature : FoldingFeature?) : Boolean { contract { returns(true) implies (foldFeature != null) } return foldFeature?.state == FoldingFeature.State.HALF_OPENED && foldFeature.orientation == FoldingFeature.Orientation.VERTICAL } @OptIn(ExperimentalContracts::class) fun isSeparating(foldFeature : FoldingFeature?) : Boolean { contract { returns(true) implies (foldFeature != null) } return foldFeature?.state == FoldingFeature.State.FLAT && foldFeature.isSeparating }
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) { if (isTableTopPosture((FoldingFeature) feature)) { enterTabletopMode(feature); } else if (isBookPosture((FoldingFeature) feature)) { enterBookMode(feature); } else if (isSeparating((FoldingFeature) feature)) { // Dual-screen device if (((FoldingFeature) feature).getOrientation() == FoldingFeature.Orientation.HORIZONTAL) { enterTabletopMode(feature); } else { enterBookMode(feature); } } else { enterNormalMode(); } } } } } private boolean isTableTopPosture(FoldingFeature foldFeature) { return (foldFeature != null) && (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) && (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL); } private boolean isBookPosture(FoldingFeature foldFeature) { return (foldFeature != null) && (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) && (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL); } private boolean isSeparating(FoldingFeature foldFeature) { return (foldFeature != null) && (foldFeature.getState() == FoldingFeature.State.FLAT) && (foldFeature.isSeparating() == true); }
On dual-screen devices, use layouts designed for tabletop and book postures even if the FoldingFeature
state is FLAT
.
Don't place UI controls too close to a fold or hinge when isSeparating
is true because the controls can be difficult to reach. Use occlusionType
to decide whether to place content within the folding feature bounds
.
Window size changes
An app's display area can change as a result of a device configuration change—for example, when the device is folded or unfolded, rotated, or a window is resized in multi-window mode.
The Jetpack WindowManager WindowMetricsCalculator
class enables you to retrieve the current and maximum window metrics. Like the platform WindowMetrics
introduced in API level 30, the WindowManager WindowMetrics
provide the window bounds, but the API is backward compatible down to API level 14.
Use WindowMetrics
in an activity’s onCreate()
or onConfigurationChanged()
method to configure your app's layout for the current window size, for example:
Kotlin
override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) val windowMetrics = WindowMetricsCalculator.getOrCreate() .computeCurrentWindowMetrics(this@MainActivity) val bounds = windowMetrics.getBounds() ... }
Java
@Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); final WindowMetrics windowMetrics = WindowMetricsCalculator.getOrCreate() .computeCurrentWindowMetrics(this); final Rect bounds = windowMetrics.getBounds(); ... }
See also Support different screen sizes.
Additional resources
Samples
- Jetpack WindowManager: Example of how to use the Jetpack WindowManager library
- Jetcaster: Tabletop posture implementation with Compose