Skip to main content

foldable_info 0.3.0 copy "foldable_info: ^0.3.0" to clipboard
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.

iPhone Duo closed, partly open and flat simulator captures

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
Before avoidance After avoidance

Fold, occlusion and margins explained

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.
  • isFoldable with explicit availability: missing evidence stays unknown.
  • Distinct fold, hinge and blocked regions, 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.
  • FoldableSafeArea places 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.

2
likes
160
points
203
downloads
screenshot

Documentation

API reference

Publisher

verified publisherviuniverse.com

Weekly Downloads

Native fold geometry, hinge angles and reserved regions for iPhone Duo and Android foldables, with margins and a safe-area widget.

Repository (GitHub)
View/report issues
Contributing

Topics

#foldable #dual-screen #hinge #adaptive-ui #iphone-duo

License

MIT (license)

Dependencies

collection, flutter

More

Packages that depend on foldable_info

Packages that implement foldable_info