Skip to main content

duo_rail_kit 0.1.1 copy "duo_rail_kit: ^0.1.1" to clipboard
duo_rail_kit: ^0.1.1 copied to clipboard

Flutter layout helpers and an iOS plugin for system-directed side rails and safe-area spacing on iPhone Duo.

duo_rail_kit #

A Flutter plugin for placing custom navigation rails on the physical edge chosen by iOS on iPhone Duo, with shared safe-area and floating-action-button helpers. Applications retain their own navigation, appearance, breakpoints, and content. The only runtime dependency is Flutter.

MIT licensed, with source available on GitHub. See the publication checklist.

The example app on the iOS 27.2 Simulator:

[iPhone Duo inner display with the rail in the system's right column]

iPhone Duo inner display: the rail sits in the system's right column.

Outer display, right rail Left Split View app, left rail iPhone 17, bottom bar
[iPhone Duo outer display with a right-hand rail] [Left Split View app on iPhone Duo with a left-hand rail] [iPhone 17 with bottom navigation]

Requirements #

  • Package constraints: Flutter >=3.47.8 and Dart ^3.12.0. Development uses Flutter 3.47.8, selected by .fvmrc; other SDK versions need validation.
  • iOS integration: Swift Package Manager, with an iOS deployment target of at least 15.0. CocoaPods integration is not provided.
  • Native compilation: Xcode with the iOS 27.1 SDK, which declares UITraitCollection.verticalBarEdge. Runtime access is guarded for iOS 27.1.
  • Other platforms use the application's width-based rail rule; the package has no Android native code.

Flutter's generated plugin registrant registers DuoRailKitPlugin. No manual AppDelegate registration or application-specific Xcode project edits are needed. The Swift package relies on Flutter's generated FlutterFramework package; it is built through a Flutter iOS host, not directly with swift build in isolation.

Installation #

Add the hosted package to your application's pubspec.yaml:

dependencies:
  duo_rail_kit: ^0.1.1

Run flutter pub get and commit the application's updated pubspec.lock. For versions and API documentation, see pub.dev.

For applications retaining the earlier Git dependency, the 0.1.0 release tag remains fixed and available:

dependencies:
  duo_rail_kit:
    git:
      url: https://github.com/zawasystem/duo_rail_kit.git
      ref: '0.1.0'

The HTTPS URL resolves without authentication on developer machines, in CI, and for Dependabot. Commit the application's updated pubspec.lock so the tag's resolved commit is reviewable and reproducible. See Git dependency access for CI setup and dependency verification.

For local package development, use duo_rail_kit: {path: ../duo_rail_kit} instead, adjusted for your directory layout.

Root integration #

Wrap the application once, in this order:

import 'package:duo_rail_kit/duo_rail_kit.dart';
import 'package:flutter/material.dart';

MaterialApp(
  builder: (context, child) => VerticalBarEdge(
    child: SideStatusBarInsets(child: child ?? const SizedBox.shrink()),
  ),
  home: const Scaffold(body: Center(child: Text('Hello'))),
);

SideStatusBarInsets must be below VerticalBarEdge, because it reads the system edge when the window has no horizontal safe-area column. Keep these wrappers mounted even when the device or layout changes. Changing the wrapper structure can recreate navigation and page state.

Run the example #

The example app includes iOS and Android hosts. Clone the repository to use its pinned SDK and development tools, then run:

cd example
fvm flutter pub get
fvm flutter run

Its Layout, State, and About destinations demonstrate rail placement, safe areas, FAB positioning, and state preservation. Normal launches read the actual native edge and fall back to a 600-point width rule. The images above are Simulator screenshots of this normal entry point; see doc/images/README.md for how they were taken. They show the edge delivered by the Simulator, not behavior on physical hardware. The separate DuoRailPreviewApp fixture supplies simulated geometry for tests and local previews; it is not evidence of native event delivery. See example/lib/app.dart for the complete integration.

API overview #

API Purpose
VerticalBarEdge / VerticalBarEdgeScope Observe the native edge and expose it to descendants.
HorizontalEdge A physical screen edge: left or right.
SideStatusBarInsets Apply the top-only Duo spacing rule and expose the added inset.
RailPlacement Select an edge, calculate occupied width, and prepare rail safe areas.
RailSideRow Place a rail beside keyed content without changing the content's position in the widget tree.
RailSideFab / RailSideFabLocation Opt in to a consistent FAB bottom margin beside a bottom-aligned rail.

Selecting and laying out a rail #

Read placement from the shell context, before changing content insets:

final placement = RailPlacement.of(
  context,
  showsRailByWidth: (windowWidth) => windowWidth >= 600,
);
final railSafeArea = RailPlacement.railSafeAreaOf(
  context,
  receivesAddedTop: false,
);
final railWidth = placement?.followsSystemBar == true
    ? RailPlacement.statusColumnWidth
    : 80.0;
final occupiedWidth = placement?.occupiedWidth(railSafeArea, railWidth) ?? 0.0;
final railPadding = placement?.centredRailSafeArea(railSafeArea) ?? railSafeArea;

The breakpoint and ordinary rail width above are example application choices. An OS edge takes precedence over the width rule. Without one, the width rule selects a left rail or no rail. The width rule receives the full window width, not a pane's layout constraints.

Use RailSideRow(railEdge: placement?.edge, rail: rail, content: content) to keep the content in a stable keyed slot. Size the rail's outer box to occupiedWidth, pass railPadding through the rail's MediaQuery, and prevent the content from counting the already occupied horizontal inset a second time. Keep application wrappers around the content stable as placement changes. RailSideRow does not adjust safe areas, render a rail, or implement navigation on its own.

Use the unmodified railSafeArea with occupiedWidth. Use centredRailSafeArea(railSafeArea) for a vertically centered rail and for any bottom-aligned item group that can grow into the system status display (large text, many destinations, or a keyboard-shortened window). It clears the rail's own horizontal inset and reserves the top status band only when that column exists and the status bar is visible. Make tall rails scrollable, for example with NavigationRail(scrollable: true).

Applications that omit centredRailSafeArea must handle the rail's own horizontal inset and verify that their item group remains below the status display at all supported text sizes and keyboard heights. Styling, labels, alignment, grids, SnackBars, overlays, and content/FAB avoidance remain application responsibilities.

Physical edges and measured geometry #

Native leading/trailing values are converted using UIKit's layout direction. All rail APIs then use physical left/right. RailSideRow fixes the row layout to LTR while preserving the content's own text direction.

The existing geometry is based on iOS 27.1 simulator measurements:

Duo configuration System edge and column
Outer portrait / inner landscape Right, with an 84-point column.
Outer landscape Left or right, with an 84-point column and hidden status bar.
Split View, left window Left, without a horizontal status column.
Split View, right window Right, with an 84-point column.
Inner portrait No vertical edge; application width rule applies.

RailPlacement.statusColumnWidth is 84 logical pixels. RailPlacement.statusColumnBottom is 152: a conservative common lower boundary for the visible status display. It may reserve more space than necessary on the inner display. These are measured constants, not dynamically reported dimensions.

Top safe-area spacing #

SideStatusBarInsets raises only the top MediaQuery.padding and viewPadding to at least 16 when the original top is zero and either:

  • Exactly one horizontal edge has a safe-area inset; or
  • Both horizontal insets are zero and the native vertical edge is present.

This is a geometry rule, not a device-model lookup. Left, right, and bottom are unchanged. Shapes with two horizontal insets, such as ordinary landscape iPhones, are excluded even if a vertical edge is reported.

addedOf reports the added top inset; isActiveOf reports whether any inset was added. removeAdded restores the underlying OS values. An active inset does not necessarily mean a horizontal status column exists: the left Split View window also receives top spacing.

railSafeAreaOf(receivesAddedTop: false) restores OS insets for the rail. With true, the rail receives the added top only for a one-sided-column shape, never for the left Split View window without a column.

FAB placement #

Always wrap the shell content in RailSideFab, and change only its enabled value. Enable it beside a bottom-aligned rail following the system edge:

RailSideFab(
  enabled: placement?.followsSystemBar ?? false,
  child: Builder(
    builder: (context) => Scaffold(
      floatingActionButtonLocation: RailSideFab.locationOf(context),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.add),
      ),
    ),
  ),
);

The FAB bottom is 24 logical pixels above the Scaffold's contentBottom (which may be the keyboard or an application-owned panel). The bottom safe-area inset is not added: with an inset of 34, the FAB overlaps that band by 10. Horizontal placement follows Flutter's standard endFloat, including RTL behavior. Keep the constant location returned by locationOf; replacing it with a fresh location object can trigger an unwanted Scaffold FAB transition. Applications can use enabledOf to keep their content clear of the FAB.

Native contract and limitations #

  • Channel: com.zawasystem.fleet/vertical_bar_edge.
  • Request: getEdge; notification: edgeChanged.
  • Payload: {edge: "left" | "right" | "none", statusBarHidden: bool, sequence: int}.
  • Dart applies only newer sequence numbers. Request replies and trait events can arrive out of order; arrival order must not replace sequence ordering.
  • The first frame has no native edge and uses the width rule. The column-free left Split View window receives its top spacing after the first native reply; a one-sided-column window can receive spacing immediately from its geometry.
  • statusBarHidden is sampled on queries and trait changes, not independently observed. Dart queries again on size/view-padding changes and app resume. A visibility-only change can remain stale until the next refresh.
  • Missing plugin/channel errors fall back to width-based placement. Successful Dart tests alone therefore do not prove that native registration works.

Development and validation #

Clone the GitHub repository and run from its root. The .fvmrc and development scripts belong to the source checkout and are excluded from the pub.dev archive.

fvm flutter pub get
fvm flutter analyze
fvm flutter test

On macOS with Xcode 27.1 or newer and the matching iOS Simulator SDK:

fvm exec bash tool/build_ios_host.sh

The script creates a temporary iOS-only Flutter host, explicitly enables SPM in that host's pubspec, adds this package as a local dependency, and copies the example entrypoint. It builds an unsigned debug simulator app and checks the generated SPM manifest, GeneratedPluginRegistrant, and defined symbols for the plugin class and registration method in the compiled app. It removes the host on exit, including failed builds. It does not install, launch, or remove any application on a device or simulator.

The tests cover geometry, stable widget state, RTL, asynchronous event ordering, safe-area behavior, FAB positioning, and the Dart/Swift channel contract. The source-level Swift tests do not compile or execute native code; the host check adds compilation and link validation. It does not prove runtime event delivery. Use simulator/device checks for native events, rotation, folding, Split View, and the status display before releasing an integration.

CI and native builds #

The CI workflow runs package and example checks on Ubuntu. Native compilation is manual: dispatch the workflow from main with native_ios enabled. It uses a GitHub-hosted macOS runner and fails explicitly if the required iOS 27.1 SDK is unavailable. Native jobs do not run automatically on pushes or pull requests.

A configured workflow does not establish a successful run: check its result for the commit being integrated. See the publication checklist for release validation steps.

Public pull requests must use an isolated hosted or ephemeral environment. Do not run untrusted pull-request code on a persistent self-hosted runner or expose credentials to it. Review runner access and SDK availability before enabling native jobs.

Contributing and release preparation #

See CONTRIBUTING.md for the development workflow, MIGRATION.md for dependency migration, and CHANGELOG.md for package changes. Release checks are in docs/publication.md.

Security reporting is described in SECURITY.md. The package is licensed under the MIT License. Flutter template, font, and icon attributions are in THIRD_PARTY_NOTICES.md.

1
likes
0
points
899
downloads

Publisher

verified publisherzawasystem.com

Weekly Downloads

Flutter layout helpers and an iOS plugin for system-directed side rails and safe-area spacing on iPhone Duo.

Repository (GitHub)
View/report issues

Topics

#adaptive-layout #navigation #foldable #safe-area

License

unknown (license)

Dependencies

flutter

More

Packages that depend on duo_rail_kit

Packages that implement duo_rail_kit