foldable_info

Read fold state, hinge angle, view size and blocked areas in Flutter. Your app or design system decides how to arrange content; this package supplies data.

Version 0.2.0 adds native Android support. Android reads Jetpack WindowManager fold features and the hinge-angle sensor when present. The iOS bridge reads public UIKit APIs when available. Web and desktop use Flutter view metrics and display features. Version 0.1.1 was iOS-first. The new Apple hinge/reserved-region APIs are currently documented as beta and may change. Missing data is reported explicitly, not guessed.

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.

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 provide UI providers or builders. 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.

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.

Platform support

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.
  • No layout placement, navigation, 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 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.

Libraries

foldable_info
Fold and view measurements, native availability and coordinate conversion.
testing
Test doubles. Import separately from the production entrypoint.