On a foldable Android device, how does a Flutter app locate a hinge or fold, and how do you keep content from straddling it?
answer
- MediaQuery.displayFeaturesOf
- bounds in logical pixels
- fold, hinge, cutout types
- postureFlat versus postureHalfOpened
- DisplayFeatureSubScreen and anchorPoint
basics
~20 sMediaQuery.displayFeaturesOf returns DisplayFeature objects with logical-pixel bounds, a type (fold, hinge, cutout) and a posture state; the list is populated only on Android. Split panes at a hinge's bounds, or wrap content in DisplayFeatureSubScreen to confine it to one side.
solid answer
~40 s`MediaQuery.displayFeaturesOf(context)` gives a `List<DisplayFeature>`, populated only on Android and empty elsewhere. Each has `bounds`, a `Rect` in **logical pixels** in the view's coordinate space; `type`, which is `fold`, `hinge`, `cutout` or `unknown`; and `state`, which for folds and hinges is the posture, `postureFlat` or `postureHalfOpened`. A hinge or cutout obstructs the display, while a fold is a crease that may have zero width. To avoid straddling it, lay out two panes with the split at the feature's `bounds`, or wrap a single piece of content in `DisplayFeatureSubScreen`, which picks the sub-screen nearest its `anchorPoint`; dialog routes already do this. Also treat unfolding as a window-size change: keep state above the layout branch and do not lock orientation, which can letterbox the app.
code
dart · 32 linesimport 'dart:ui' show DisplayFeature, DisplayFeatureType;
import 'package:flutter/material.dart';
class HingeAwareSplit extends StatelessWidget {
const HingeAwareSplit({super.key, required this.list, required this.detail});
final Widget list;
final Widget detail;
@override
Widget build(BuildContext context) {
final Size size = MediaQuery.sizeOf(context);
DisplayFeature? divider;
for (final DisplayFeature f in MediaQuery.displayFeaturesOf(context)) {
final bool splits = f.type == DisplayFeatureType.hinge || f.type == DisplayFeatureType.fold;
if (splits && f.bounds.height >= size.height) {
divider = f;
}
}
if (divider == null) {
return Row(children: <Widget>[SizedBox(width: 320, child: list), Expanded(child: detail)]);
}
return Row(
children: <Widget>[
SizedBox(width: divider.bounds.left, child: list),
SizedBox(width: divider.bounds.width),
Expanded(child: detail),
],
);
}
}go deeper
Know that foldables report hinges and folds through MediaQuery display features, and that unfolding changes the window size.
Explain DisplayFeature's bounds, type and state, that the list is Android-only, and what DisplayFeatureSubScreen does with anchorPoint.
Design a list-detail split that honours a hinge, survives fold and unfold without losing state, and avoids letterboxing from orientation locks.
Judge how far to invest in posture-specific layouts versus plain width breakpoints, given the device share and testing cost.
## What Flutter reports Foldables add a **display feature**: a physical or visual boundary within the app's view. Flutter exposes them through `MediaQueryData.displayFeatures`, read with `MediaQuery.displayFeaturesOf(context)`. The list is **populated only on Android**; on other platforms and on devices without such features it is empty, so code that uses it must treat an empty list as the normal case. Each `DisplayFeature` (from `dart:ui`) has three fields: | Field | Meaning | |---|---| | `bounds` | a `Rect` in **logical pixels**, in the coordinate space spanning all screens in use | | `type` | `DisplayFeatureType.fold`, `hinge`, `cutout` or `unknown` | | `state` | the posture for folds and hinges: `postureFlat`, `postureHalfOpened` or `unknown`; always `unknown` for cutouts | The types differ in whether they block drawing: - a **hinge** is a physical gap between two panels and obstructs the display; - a **cutout** houses cameras or sensors and obstructs its area; - a **fold** is a crease in a flexible screen; its `bounds` can be zero-width and it does not block pixels, but content across it is awkward to read, especially when half-opened. ## Keeping content off the feature ### Two panes split at the hinge For a list-detail app, a hinge is a natural divider. Find a feature whose `type` is `hinge` or `fold` and whose bounds span the full height, then size the left pane to `bounds.left` and start the right pane at `bounds.right`. With a vertical hinge, `bounds.left` is the width of the left screen and `bounds.right` is that width plus the hinge's. ### One piece of content on one side `DisplayFeatureSubScreen` does this automatically. It treats a feature as splitting the screen when: 1. the feature obstructs — its area is not zero, **or** its state is `postureHalfOpened`; and 2. it spans the full height (left and right sub-screens) or full width (top and bottom sub-screens). It then places its child in the sub-screen closest to `anchorPoint`. If `anchorPoint` is null, the text direction decides: top-left for left-to-right, top-right for right-to-left. The child receives a new `MediaQuery` whose display features and padding are relative to that sub-screen. Flutter's dialog routes already wrap their page in it, and `showDialog` accepts an `anchorPoint`, so dialogs do not open across the hinge. ## Posture changes A half-opened device can be used like a laptop, with content on the top half and controls on the bottom. `state` tells you which posture is current, and the list changes as the user folds, which triggers a rebuild for widgets that read it. ## Unfolding is a resize Folding and unfolding change the **window size**, often from phone-width to tablet-width. Everything about responsive layouts applies: - branch on width, not on "is foldable"; - keep selection and form state above the layout branch so it survives the switch; - do not lock orientation with `SystemChrome.setPreferredOrientations`. Flutter's docs warn that a portrait lock can make Android show the unfolded app **letterboxed** in a compatibility mode, and then `MediaQuery` never reports the larger size. If an app truly must reason about the physical panel, the `Display` object from `View.of(context).display` reports the physical size and pixel ratio; the docs call this one of the few cases for physical dimensions. ## Testing Tests can supply a `MediaQueryData` with a fake `displayFeatures` list to check the split without a device, and a foldable emulator covers posture changes by hand.
- Why might a portrait-locked Flutter app look letterboxed when a foldable is opened?Locking orientation with `SystemChrome.setPreferredOrientations` can make Android run the unfolded app in a portrait compatibility mode: a centred window with black bars. `MediaQuery` then reports only that small window, so a responsive layout never sees the larger size. Supporting all orientations avoids it.
- When does DisplayFeatureSubScreen treat a fold as splitting the screen?When the feature obstructs — non-zero area, or a `postureHalfOpened` state — and spans the full height or width of the screen. A zero-width fold in the flat posture does not split, so content may still cross it.
saying these in an interview costs you the question
- displayFeatures is populated on both Android and iOS.
- DisplayFeature bounds are reported in physical pixels.
- A fold always has non-zero width and always blocks drawing.
- Locking to portrait is the safe choice on foldables.
- Branching on a known foldable model list is enough.