foldable_info 0.3.0
foldable_info: ^0.3.0 copied to clipboard
Native fold geometry, hinge angles and reserved regions for iPhone Duo and Android foldables, with margins and a safe-area widget.
foldable_info #
Native fold geometry for iPhone Duo and Android foldables: fold posture, hinge angles, reserved regions and margins, plus a small widget to keep controls clear of obstacles. Flutter 3.38+. No provider or design-system dependency.

Actual iPhone Duo / iOS 27.1 simulator captures: closed → partly open landscape → flat portrait. This is a sequence of measured poses, not a hardware recording.
| Avoidance off: control crosses the fold | Avoidance on: control stays in a free region |
|---|---|
![]() |
![]() |

Orange regions cover content; purple regions divide it. iOS frames already
include margins. FoldableSafeArea uses those frames to keep its child in the
largest free rectangle. Your app still chooses its navigation and pane layout.
Why foldable_info? #
- Native UIKit reserved regions and Jetpack WindowManager folds; Android hinge sensor readings when available, separate from layout updates.
isFoldablewith explicit availability: missing evidence staysunknown.- Distinct
fold,hingeandblockedregions, including inactive regions. - Margins exposed as
extraSpace; conversion into nested widget coordinates. - Opt-in
FoldableSafeArea, shared streams, and an injectable fake for tests.
Install #
flutter pub add foldable_info
Flutter 3.38+ is required. The Dart language minimum is 3.9, so the Dart 3.10 beta bundled with Flutter 3.38.0 is also accepted. iOS deployment target: 15.0+. New fold APIs need a supporting OS and device/view hierarchy. There are no extra permissions, provider dependency or design-system dependency.
Keep a control clear of reserved regions #
import 'package:flutter/material.dart';
import 'package:foldable_info/foldable_info.dart';
// Place in a bounded box, e.g. Scaffold.body or Expanded.
StreamBuilder<FoldableData>(
stream: FoldableInfo.watch(),
builder: (context, snapshot) {
final data = snapshot.data;
if (snapshot.hasError) return const Text('Could not read geometry');
if (data == null) return const SizedBox.shrink();
return FoldableSafeArea(
data: data,
child: Align(
alignment: Alignment.topRight,
child: FilledButton(onPressed: () {}, child: const Text('Action')),
),
);
},
)
The widget applies Flutter SafeArea first, then avoids intersecting blocked
and hinge frames. It ignores explicitly inactive regions. Add AreaType.fold
to avoid to keep the whole child on one side of a fold. It chooses one largest
free rectangle, so a small corner obstacle can reserve an entire strip. It does
not split the child into panes. Equal choices prefer top, then leading side.
Use useSafeArea: false for an intentional edge-to-edge region. Ancestor
SafeArea padding is consumed normally and is not added twice. Geometry is
measured after layout; the child appears on the following frame. Moving
ancestors are remeasured after each rendered frame. Unavailable/stale data or an
unsupported transform falls back to Flutter safe padding alone; that fallback
cannot guarantee avoidance of an unreported obstacle. See geometry rules.
Read once #
import 'package:foldable_info/foldable_info.dart';
try {
final data = await FoldableInfo.read();
final size = data.view.value?.size;
final state = data.folds.value?.firstOrNull?.openState.value;
print('Size: $size, fold: ${state?.name ?? data.folds.state.name}');
} on FoldableError catch (error) {
print('${error.code.name}: ${error.message}');
}
The direct methods use the engine's implicit Flutter view. They need no
BuildContext. In a multi-view app, select a view explicitly:
final handle = FoldableInfo.instance.forView(View.of(context));
final data = await handle.read();
Import package:flutter/widgets.dart for View. Native Android/iOS currently support
the engine's attached view (Flutter view ID 0); another view reports noView.
Android also needs an attached Activity. Activity recreation is handled automatically.
The service never chooses an arbitrary native window. Engines have independent
services. Static methods report noView when no implicit Flutter view exists.
Watch changes #
final subscription = FoldableInfo.watch().listen(
(data) { /* update your state */ },
onError: (Object error) { /* handle the failure */ },
);
// In dispose, cancel only your own subscription.
await subscription.cancel();
watch() replays the latest snapshot. It shares native observation with other
listeners on the same view. Removing one listener does not stop the others.
View changes and native layout changes update the data. sessionId identifies
one observation session; compare revision only within that session. A different
observation timestamp alone does not advance the layout revision.
Use Flutter's own builder to display live data:
StreamBuilder<FoldableData>(
stream: FoldableInfo.watch(),
builder: (context, snapshot) {
if (snapshot.hasError) return Text('Could not read fold data');
final data = snapshot.data;
if (data == null) return const Text('Loading');
return Text(data.folds.value?.firstOrNull?.openState.value?.name
?? data.folds.state.name);
},
)
Import package:flutter/material.dart for this widget example. The stream for
a live handle is stable across calls, so a rebuild does not restart it. The
package does not require a state-management provider. Provider, Riverpod, Bloc or
your design system can consume the same streams.
Read the angle separately #
final sample = await FoldableInfo.readAngles();
final degrees = sample.angles.value?.firstOrNull?.degrees;
final subscription = FoldableInfo.watchAngles().listen(
(sample) { /* update an angle effect */ },
onError: (Object error) { /* handle the failure */ },
);
await subscription.cancel();
Angle-only updates do not publish a new layout snapshot. Listen only while the
angle effect is active. Angles are in degrees; frames, padding and insets are
in Flutter logical pixels. The plugin uses the OS fold status; it does not
invent closed/partly-open/open thresholds from the angle. An angle list may have
no known foldId; do not join it to an area by guessing its index.
Understand unavailable data #
Each measured group is a DataValue<T>. Only ready contains a value.
| State | Meaning |
|---|---|
ready |
A value was measured, including an empty list. |
loading |
Waiting for the first valid measurement or matching geometry. |
notAvailable |
The API exists, but there is no current value in this hierarchy/state. |
notSupported |
This platform or API cannot provide that field. |
unknown |
Available evidence cannot answer the question. |
error |
Reading or decoding failed; see error. |
Ready values have a source and UTC readAt. The area, fold and angle lists in
snapshots are immutable. Read failures complete the Future with FoldableError;
stream failures also reach onError. A successful later measurement can recover.
Default read timeout: 5 seconds. Invalid timeouts throw ArgumentError.
Read foldability explicitly; null below means the evidence cannot answer yet:
final data = await FoldableInfo.read();
final bool? foldable = data.isFoldable.value; // true / false / unknown (null)
// Inspect data.isFoldable.state to distinguish loading/unknown/unsupported.
An empty area list does not prove the device cannot fold. isFoldable remains
unknown without evidence. ViewArea.isActive says whether that reserved area is
active; it does not identify which physical screen is powered on. Fold state and
area activity describe different things and can change independently.
Convert areas to widget coordinates #
Native areas use the complete Flutter view's origin. A widget inside SafeArea,
a pane or a translated container can have a different origin. After layout:
final local = FoldableSpace.toBox(
data: data,
target: context.findRenderObject()! as RenderBox,
view: View.of(context),
);
if (local.state == LocalState.ready) {
// local.areas now use this widget's origin and are clipped to its box.
}
Import package:flutter/rendering.dart and package:flutter/widgets.dart.
The helper supports translation and positive axis-aligned scale, including
zero-width fold lines. It rejects rotation, skew, reflection, perspective,
another view and changed size/pixel ratio. Call again after layout or a new data
snapshot. iOS reserved frames already include the reported margins; do not add
extraSpace again to those frames. It is metadata, scaled during conversion.
Device support #
Native API support is different from tested hardware. The matrix below records simulator/emulator evidence from verification; it does not certify physical devices. Detection follows OS capabilities, never a hard-coded model-name list.
| Device / family | Geometry and posture | Angle | Evidence / limitations |
|---|---|---|---|
| iPhone Duo | UIKit reserved occlusion/division regions, margins, activity, hinge status and bar-side hints when exposed | UIKit hinge | iOS 27.1 simulator: closed, partial and flat poses; physical device unverified. Build host app with the iOS 27.1 SDK for full Duo geometry. |
| Pixel 10 Pro Fold | Native WindowManager fold, separating/occlusion properties; cover cutout via Flutter | Sensor when present | API 37 emulator: half-open, flat and foreground cover-display transitions. Physical device unverified. |
| Galaxy Z Fold / Z Flip, other Pixel Fold | Same Android APIs when supplied by vendor | Requires API 30+ and sensor | API-based support; these models not individually verified. |
| Microsoft Surface Duo / Duo 2 | WindowManager hinge gap and separating geometry when exposed | Not guaranteed | API-based support; hardware/emulator not individually verified. Surface Duo is distinct from iPhone Duo. |
| Ordinary iPhone / iPad | View metrics, safe area and size classes; reserved regions only if OS/view exposes them | Usually unavailable | iPhone 18 Pro iOS 27.0 and iPad iOS 26.2 simulator fallback checks recorded. No fold data does not prove hardware incapable. |
| Ordinary Android phones | View metrics and Flutter cutouts | Usually unavailable | Pixel 7 emulator consumer check recorded. |
| Web, macOS, Windows, Linux | Flutter metrics/display-feature fallback | Unsupported | No native fold-hardware detection; do not interpret empty features as isFoldable == false. |
Minimum iOS deployment target is 15; newer fold APIs need a supporting runtime, device and view hierarchy. Older systems retain supported metrics and report unsupported fold fields explicitly. Read iOS setup and troubleshooting for SDK, scene lifecycle and empty-region checks.
Platform data #
| Data | iOS | Android | Web / desktop |
|---|---|---|---|
| View size, safe padding, keyboard/gesture insets | Flutter | Flutter | Flutter |
| Fold/blocked areas | UIKit reserved regions when available | Native WindowManager folds + Flutter cutouts | Flutter displayFeatures, often empty |
| Fold state | UIKit hinge status when available | Native FLAT / HALF_OPENED |
Usually unavailable |
| Hinge angle | UIKit hinge angle when available | TYPE_HINGE_ANGLE on API 30+ with a sensor |
Not supported |
| Size classes, native safe area, preferred bar side | UIKit | Not supported | Not supported |
Android requires API 24 or newer. Hinge angles require API 30+ and a sensor; there are no additional permissions. WindowManager depends on vendor support. An empty feature list does not prove that the device cannot fold. Web/desktop labels describe Flutter fallback support, not native fold hardware detection.
The iOS bridge uses public
UIHingeInteraction
and reserved-region APIs, guarded at runtime. Older systems still expose size
classes and safe area. Resolve BarSide.start/end using your widget's
Directionality. A preferred bar edge does not prove a toolbar is present.
Limits #
- Does not enumerate every UIKit toolbar, app overlay, floating/split keyboard, rounded corner or design spacing. Add app-owned obstacles in your layout layer.
- No physical-screen identity, six named device poses or hardware model detection.
- Native Android/iOS multi-view support beyond the attached engine view is not implemented.
- Android observes the default hinge sensor, without assigning it to a fold ID. Angle-only changes do not alter layout state; sensor precision and availability depend on the device.
FoldableSafeAreaplaces content in one free rectangle. No navigation, two-pane composition, overflow decisions or animation.- Minimum-version declarations are not a complete tested device/SDK matrix. See verified coverage and iOS details and Android details.
Test and contribute #
Import package:foldable_info/testing.dart for FakeFoldableInfo. Provide
fixtures keyed by view ID, call fake.forView(view) and use sendData,
sendAngles or sendError. Counters must advance within a session. Call
dispose() when finished; no global service replacement is needed.
The example includes a live region overlay and a copyable geometry snapshot for bug reports. It uses only Flutter and this plugin, including angle subscription lifetime and keyboard metrics. In a standalone checkout:
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter test
flutter run -d <device-id>
See the API overview and class diagram, CONTRIBUTING, release steps, CHANGELOG and security reporting. Licensed under MIT.


