duo_dynamic_sizing 1.0.2
duo_dynamic_sizing: ^1.0.2 copied to clipboard
Adaptive layout for the foldable iPhone Duo and any screen. Fold-aware modes, split panes, side navigation, safe areas, media fit and a pose simulator.
Made by @Abojawdat
One context.duo that knows if the phone is folded, open, sideways or sharing the screen.
Widgets that lay out, navigate and keep state the way iOS 27 does on Apple's first foldable.
Pure Dart. Works today on iOS, Android, web and desktop, in English, Arabic and every other language.
واجهة واحدة context.duo تعرف إن كان الهاتف مطوياً أو مفتوحاً أو مائلاً أو يتقاسم الشاشة مع تطبيق آخر.
مكتوبة بلغة Dart فقط، وتعمل اليوم على iOS وAndroid والويب وسطح المكتب، بالعربية والإنجليزية.
English #
See it in action #
Every clip below is the real example app running this package, rendered frame by frame. Nothing is mocked up.
1. Fold and unfold #
What happens: closed, the email fills the 466 pt outer screen. Open the phone and the list slides back in beside the same email, still scrolled where you left it.
Why: Apple treats continuity across a fold as a requirement. People fold in the middle of reading and expect to land exactly where they were. Nothing here rebuilds from scratch: the package moves the same widget element to its new place.
Code: DuoListDetail inside DuoNavigationScaffold, nothing else.
2. Split View #
What happens: a second app joins. Yours shrinks live from 951 to 475 pt. The moment it becomes a Split View slice, its rail jumps from the island side to its own outer edge, and the list collapses to one pane.
Why: that's where iOS 27 puts the bars of the left-hand app. Every intermediate width is laid out for real, so there's no snapping between a few hand-picked sizes.
Code: nothing extra. context.duo.isSplit tells you when it happens.
3. Half open: tabletop and book #
What happens: on a table, the video jumps above the fold and the controls drop to the bottom half. Held like a book, the controls slide to the trailing half.
Why: a bent screen has two surfaces. Content that straddles the crease is hard to read and hard to tap, and Apple's guidance says the same.
Code: DuoSplit for the two halves, DuoAvoidFold for floating controls, context.duo.posture to decide.
4. Rotation #
What happens: closed and turned sideways, the island moves to the top and the rail stays on the side. Open and turned upright, the rail becomes a bottom bar, the one pose where iOS keeps horizontal bars.
Why: the Duo's inner screen ignores orientation locks, so your layout has to follow the window, not the device orientation.
Code: DuoNavigationScaffold decides with context.duo.prefersRail.
5. Resizable windows #
What happens: a desktop window is dragged from 360 to 1240 pt. The mode, the number of panes and the navigation all update on every frame.
Why: the same rules that handle the Duo handle iPad Stage Manager, web browsers and desktop windows. One set of layout decisions covers every screen.
Code: the same app, with no platform checks.
6. English and Arabic #
What happens: the app switches to Arabic. The list moves to the right and the detail to the left, while the split stays exactly on the physical fold.
Why: reading direction decides where the primary pane goes; the hinge decides where the split goes. Mixing those two up would put content on the crease.
Code: automatic from your app's Directionality. Override with textDirection: and railSide:.
7. Media fit #
What happens: the same 16:9 video cycles through contain (bars), cover (cropped) and smart, which crops only when it loses less than 15%.
Why: the inner screen is √2 shaped, so 16:9 video leaves 134 pt of black bars. Sometimes a small crop is the better trade, and sometimes it isn't.
Code: DuoMedia(aspectRatio: 16 / 9, child: player).
iPhone Duo, every pose #
| Pose | Window (pt) | What your app gets |
|---|---|---|
| Closed | 466 × 678 | One pane. The rail sits on the right, next to the vertical Dynamic Island. |
| Closed, sideways | 678 × 466 | One pane in landscape. |
| Open | 951 × 669 | Two panes, split exactly at the fold. |
| Open, upright | 669 × 951 | Two panes and a bottom bar. |
| Split View | about 475 × 669 | One pane per app, each with its rail on its own outer edge. |
And every other screen #
On Android foldables the package reads the real fold from MediaQuery.displayFeatures. Phones, tablets, web and desktop get the same widgets, driven by window size.
Install #
flutter pub add duo_dynamic_sizing
Needs Flutter 3.41+ (Dart 3.11+). There's no setup, no platform code and no permissions.
Minimal example #
A complete app in about 40 lines: a mail list that becomes list + detail when there's room, with the right navigation on every screen. It's example/lib/minimal.dart; run it with flutter run -t lib/minimal.dart.
import 'package:duo_dynamic_sizing/duo_dynamic_sizing.dart';
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: Inbox()));
class Inbox extends StatefulWidget {
const Inbox({super.key});
@override
State<Inbox> createState() => _InboxState();
}
class _InboxState extends State<Inbox> {
int tab = 0;
int? open;
@override
Widget build(BuildContext context) {
return DuoNavigationScaffold(
appBar: AppBar(title: Text(context.duo.mode.name)),
selectedIndex: tab,
onDestinationSelected: (i) => setState(() => tab = i),
destinations: const [
DuoDestination(icon: Icon(Icons.inbox), label: 'Inbox'),
DuoDestination(icon: Icon(Icons.star), label: 'Starred'),
],
body: DuoListDetail<int>(
selected: open,
onClose: () => setState(() => open = null),
empty: (_) => const Center(child: Text('Pick a message')),
list: (_) => ListView.builder(
itemCount: 30,
itemBuilder: (_, i) => ListTile(
title: Text('Message $i'),
selected: i == open,
onTap: () => setState(() => open = i),
),
),
detail: (_, i) => Center(child: Text('Message $i')),
),
);
}
}
The same code gives you all of this:
| Screen | Result |
|---|---|
| iPhone Duo closed | the list, and the message full screen once tapped; rail by the island |
| iPhone Duo open | list and message side by side, split at the fold |
| Split View | one pane, rail on the app's outer edge |
| Android foldable half open | panes split exactly at the hinge |
| iPhone | one pane, bottom bar |
| iPad, desktop, web | list and message side by side, side rail |
| Arabic or Hebrew app | everything mirrored, still split at the fold |
Reading the window yourself works anywhere:
final duo = context.duo; // rebuilds on fold, rotation and Split View
if (duo.isExpanded) { /* two panes */ }
duo.mode; // DuoMode.openLandscape
duo.safe.right; // 59, the Dynamic Island side
duo.gridColumns(160); // even across the fold
Why it's built this way #
The short version is below. The full reasoning, with the alternatives we rejected, is in DESIGN.md (العربية).
- Sizes never scale. Both Duo screens have the same physical point size, about 153 points per inch. Width-scaled 16 pt text would jump to 33 pt when you unfold, so layouts gain panes, not bigger widgets.
- Everything comes from
MediaQuery. That makes it reactive, testable and simulator-friendly, and it only rebuilds when the window really changes. - Pure Dart. It works today on every Flutter version from 3.41 and every platform, with no Xcode 27.1 or pods required.
- The iPhone Duo is detected by its exact screen sizes at 3x on iOS, so iPads, Android phones and desktop windows are never mistaken for it.
- State survives every fold because the package moves widgets instead of rebuilding them.
- Tested hard: 97 unit, widget, edge-case and golden tests, plus 9 integration tests on the real macOS app.
What's inside #
context.duo |
Mode, open or closed, panes, fold, posture, per-side safe areas, margins, grid columns. |
DuoLayout + DuoKeep |
Separate closed and open layouts that keep shared widgets' state and scroll position. |
DuoSplit |
Two panes aligned to the fold or hinge, one pane when there's no room. Flutter's take on Apple's split ArrangementView. |
DuoListDetail |
Mail-style list and detail, with system back and fold continuity handled. |
DuoNavigationScaffold |
Bottom bar or side rail, placed where iOS 27 puts its vertical bars. railSide picks the side for LTR and RTL apps. |
DuoAvoidFold |
Keeps controls off a half-open fold: trailing half in book posture, bottom half on a table. |
DuoMedia |
Fits 16:9 video to the √2 screen, cropping only when little is lost. |
DuoSimulator + DuoPose |
Every iPhone Duo pose, Android folds, iPhone and iPad, on any device and in widget tests. |
DuoDebugOverlay |
Live readout of mode, size, safe area and fold. |
context.duo #
| Member | Type | What it tells you |
|---|---|---|
mode |
DuoMode |
closedPortrait, closedLandscape, openLandscape, openPortrait, splitView, tablet, desktop |
isIphoneDuo |
bool |
The window matches an iPhone Duo screen. |
isOpen, isClosed, isSplit |
bool |
Device state. |
isExpanded, columns |
bool, int |
Room for two panes (600 × 480 and up). Drive layout off these. |
fold, foldDirection |
Rect?, Axis? |
Where the fold runs, in window coordinates. |
isSeparating, posture |
bool, DuoPosture |
Half-open fold or hinge: flat, book, tabletop, closed. |
safe, symmetricSafe |
EdgeInsets |
Per-side insets, or the mirrored maximum for centred content. |
margin |
double |
16 on compact widths, 24 otherwise (Material 3). |
prefersRail |
bool |
Side rail instead of a bottom bar. |
gridColumns(minWidth) |
int |
Columns that fit, kept even across a vertical fold. |
Change the thresholds for a subtree with DuoScope(expandedWidth: 700, child: ...).
Widgets #
Two layouts, one state. DuoKeep moves the same element between layouts instead of rebuilding it:
DuoLayout(
closed: (_) => DuoKeep(id: 'feed', child: const Feed()),
open: (_) => Row(children: [
Expanded(child: DuoKeep(id: 'feed', child: const Feed())),
const Expanded(child: Trends()),
]),
)
Panes that follow the fold:
DuoSplit(primary: const Player(), secondary: const UpNext())
Controls that dodge the crease:
Stack(children: [
const VideoPlayer(),
const DuoAvoidFold(
child: Align(alignment: Alignment.bottomCenter, child: Controls()),
),
])
Video that fits the screen:
DuoMedia(aspectRatio: 16 / 9, child: player) // smart: crops up to 15%, else letterboxes
RTL and LTR #
Everything follows your app's Directionality, so Arabic, Hebrew and Persian apps get mirrored layouts with no extra code. When panes swap sides, the split still lands exactly on the physical fold.
| LTR app | RTL app | |
|---|---|---|
DuoSplit, DuoListDetail |
Primary (list) on the left | Primary (list) on the right |
DuoAvoidFold, book posture |
Right half | Left half |
DuoSplit(ratio: .3) |
Primary takes 30% on the left | Primary takes 30% on the right |
You choose where the navigation rail goes with railSide:
railSide |
iPhone Duo | Everywhere else |
|---|---|---|
auto (default) |
Right, next to the Dynamic Island, like iOS 27's vertical bars in every language | Start side: left in LTR, right in RTL |
start |
Start side | Start side |
end |
End side | End side |
left, right |
Always that side | Always that side |
DuoNavigationScaffold(railSide: DuoRailSide.start, ...) // follow the reading direction everywhere
DuoListDetail(textDirection: TextDirection.ltr, ...) // pin one layout, whatever the app uses
Test every pose without a device #
testWidgets('unfolding keeps the draft', (tester) async {
await tester.pumpWidget(
const DuoSimulator(pose: DuoPose.closedPortrait, child: MyApp()),
);
await tester.enterText(find.byType(TextField), 'draft');
await tester.pumpWidget(
const DuoSimulator(pose: DuoPose.openLandscape, child: MyApp()),
);
expect(find.text('draft'), findsOneWidget);
});
Built-in poses: closedPortrait, closedLandscape, openLandscape, openPortrait, splitLeft, splitRight, foldableBook, foldableTabletop, iPhone, iPad. You can also build your own with DuoPose(...).
To see the live values while you develop:
MaterialApp(
builder: (context, child) =>
DuoDebugOverlay(enabled: kDebugMode, child: child!),
)
Shipping on the iPhone Duo #
- Build with the iOS 27.1 SDK (Xcode 27.1). Apps built with older SDKs run letterboxed on the inner screen.
- Adopt the UIScene lifecycle. Xcode 27 builds crash at launch without it. Flutter 3.41+ migrates an unmodified
AppDelegatefor you; see Flutter's UIScene guide. - Don't count on orientation locks. The inner screen ignores
SystemChrome.setPreferredOrientations. - Never cache sizes in
State. ReadMediaQueryorcontext.duoinbuild. - Treat left and right insets separately. The Dynamic Island runs down one side.
- Pause media on
paused, notinactive. In Split View your app stays visible whileinactive. - Keep taps off the fold when half open. Use
DuoAvoidFoldandDuoSplit.
The iPhone Duo at a glance #
| Outer screen | Inner screen | |
|---|---|---|
| Diagonal | 5.4″ | 7.6″ |
| Panel | 1398 × 2034 px, 460 ppi | 1878 × 2670 px, 430 ppi |
| Points (what Flutter sees) | 466 × 678 @3x | 669 × 951 @3x, drawn at 2007 × 2853 then scaled to the panel |
| Size class | compact | regular × regular |
Apple announced it on 9 September 2026, and it ships on 23 October 2026 with iOS 27.1. It measures 164.6 × 117.8 mm open, 84.1 × 117.8 mm closed, and weighs 254 g.
Limitations #
- No hinge angle or half-open posture on iOS yet. Flutter's iOS engine doesn't forward the iPhone Duo hinge to Dart (flutter#192515). Until it does,
postureon the Duo isclosedorunknown, and the fold is the known physical centre line. When the engine starts reporting it, this package picks it up automatically throughdisplayFeatures. For live hinge angles today, pair it with a native plugin such asfoldable. - Simulator safe areas are estimates. Apple hasn't published the Duo's inset values, so
DuoPoseuses 59 pt and 21 pt. Real devices always use the live values. - The Split View side is inferred from which side has the Dynamic Island inset.
- Native vertical bars have no Flutter equivalent.
DuoNavigationScaffoldmirrors their placement with Material widgets.
Example #
cd example
flutter run # the full showcase with a pose picker
flutter run -t lib/minimal.dart # the 40-line app above
In the showcase, pick any pose from the chip bar to simulate it on whatever device you're using, turn on RTL for the Arabic version, or choose This device to run it for real.
Contributing #
Bug reports and pull requests are welcome in the issue tracker. If something looks off on a real iPhone Duo, a screenshot with DuoDebugOverlay turned on helps a lot.
Author #
Built and maintained by @Abojawdat.
- Profile: github.com/Abojawdat
- Source: github.com/Abojawdat/iPhone-Duo-package-
If the package helps you, a star on the repo is the best way to say thanks.
العربية #
شاهدها وهي تعمل #
كل مقطع أدناه هو التطبيق التجريبي الحقيقي وهو يستخدم هذه الحزمة، مُصيَّراً إطاراً بعد إطار. لا شيء هنا مجرد تصميم وهمي.
١. الطي والفتح #
ماذا يحدث: مطوياً تملأ الرسالة الشاشة. افتحه فتعود القائمة بجانب الرسالة نفسها وفي موضع التمرير نفسه.
لماذا: المستخدم يطوي الهاتف أثناء القراءة ويتوقع أن يعود إلى المكان نفسه.
الكود: DuoListDetail داخل DuoNavigationScaffold، ولا شيء غير ذلك.
٢. العرض المقسّم (Split View) #
ماذا يحدث: ينضم تطبيق ثانٍ فيتقلص تطبيقك من 951 إلى 475 نقطة، وينتقل شريط التنقل إلى الحافة الخارجية وتصبح القائمة لوحة واحدة.
لماذا: هنا يضع iOS 27 أشرطة التطبيق الأيسر.
الكود: لا شيء إضافي. تخبرك context.duo.isSplit متى يحدث ذلك.
٣. نصف مفتوح: وضع الطاولة ووضع الكتاب #
ماذا يحدث: على الطاولة يصعد الفيديو فوق خط الطي وتنزل أزرار التحكم تحته. وفي وضع الكتاب تنتقل الأزرار إلى النصف الأخير.
لماذا: المحتوى فوق خط الطي صعب القراءة واللمس.
الكود: DuoSplit للنصفين، وDuoAvoidFold لأزرار التحكم العائمة، وcontext.duo.posture لاتخاذ القرار.
٤. التدوير #
ماذا يحدث: المطوي أفقياً يبقي شريط التنقل على الجانب، والمفتوح عمودياً يحوّله إلى شريط سفلي.
لماذا: الشاشة الداخلية تتجاهل قفل الاتجاه، فيتبع التخطيط النافذة.
الكود: يقرر DuoNavigationScaffold ذلك عبر context.duo.prefersRail.
٥. النوافذ القابلة لتغيير الحجم #
ماذا يحدث: تُسحب نافذة على سطح المكتب من 360 إلى 1240 نقطة، فيتحدث الوضع وعدد اللوحات ونوع التنقل في كل إطار.
لماذا: القواعد نفسها تغطي Duo وiPad والويب وسطح المكتب.
الكود: التطبيق نفسه، دون أي فحص للمنصة.
٦. العربية والإنجليزية #
ماذا يحدث: يتحول التطبيق إلى العربية، فتنتقل القائمة إلى اليمين والتفاصيل إلى اليسار، بينما يبقى التقسيم على خط الطي الفعلي تماماً.
لماذا: اتجاه القراءة يحدد مكان اللوحة الأساسية، والمفصل يحدد مكان التقسيم. الخلط بين الأمرين يضع المحتوى فوق خط الطي.
الكود: تلقائي حسب Directionality في تطبيقك. ويمكنك تجاوزه عبر textDirection: وrailSide:.
٧. ملاءمة الفيديو #
ماذا يحدث: يتنقل الفيديو نفسه بنسبة 16:9 بين contain (أشرطة سوداء)، وcover (مع قصّ)، وsmart الذي لا يقص إلا إذا كان الجزء المفقود أقل من 15%.
لماذا: شكل الشاشة الداخلية √2، فيترك فيديو 16:9 أشرطة سوداء بارتفاع 134 نقطة. أحياناً يكون القص البسيط هو الخيار الأفضل، وأحياناً لا يكون.
الكود: DuoMedia(aspectRatio: 16 / 9, child: player).
iPhone Duo بكل وضعياته #
| الوضعية | النافذة (نقطة) | ما يحصل عليه تطبيقك |
|---|---|---|
| مطوي | 466 × 678 | لوحة واحدة، وشريط التنقل على اليمين بجانب الجزيرة الديناميكية العمودية. |
| مطوي وأفقي | 678 × 466 | لوحة واحدة في الوضع الأفقي. |
| مفتوح | 951 × 669 | لوحتان مقسومتان على خط الطي تماماً. |
| مفتوح وعمودي | 669 × 951 | لوحتان وشريط تنقل سفلي. |
| العرض المقسّم | نحو 475 × 669 | لوحة واحدة لكل تطبيق، وشريط التنقل على الحافة الخارجية لكل منهما. |
وكل شاشة أخرى #
على أجهزة Android القابلة للطي تقرأ الحزمة خط الطي الحقيقي من MediaQuery.displayFeatures. وتحصل الهواتف والأجهزة اللوحية والويب وسطح المكتب على عناصر الواجهة نفسها، وفقاً لحجم النافذة.
التثبيت #
flutter pub add duo_dynamic_sizing
تحتاج إلى Flutter 3.41 أو أحدث (Dart 3.11 أو أحدث). لا إعدادات، ولا كود خاص بالمنصات، ولا صلاحيات.
مثال بسيط #
تطبيق كامل في نحو 40 سطراً: قائمة رسائل تتحول إلى قائمة وتفاصيل عندما تتسع المساحة، مع نوع التنقل المناسب على كل شاشة. الملف هو example/lib/minimal.dart، ويمكنك تشغيله عبر flutter run -t lib/minimal.dart.
import 'package:duo_dynamic_sizing/duo_dynamic_sizing.dart';
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: Inbox()));
class Inbox extends StatefulWidget {
const Inbox({super.key});
@override
State<Inbox> createState() => _InboxState();
}
class _InboxState extends State<Inbox> {
int tab = 0;
int? open;
@override
Widget build(BuildContext context) {
return DuoNavigationScaffold(
appBar: AppBar(title: Text(context.duo.mode.name)),
selectedIndex: tab,
onDestinationSelected: (i) => setState(() => tab = i),
destinations: const [
DuoDestination(icon: Icon(Icons.inbox), label: 'Inbox'),
DuoDestination(icon: Icon(Icons.star), label: 'Starred'),
],
body: DuoListDetail<int>(
selected: open,
onClose: () => setState(() => open = null),
empty: (_) => const Center(child: Text('Pick a message')),
list: (_) => ListView.builder(
itemCount: 30,
itemBuilder: (_, i) => ListTile(
title: Text('Message $i'),
selected: i == open,
onTap: () => setState(() => open = i),
),
),
detail: (_, i) => Center(child: Text('Message $i')),
),
);
}
}
الكود نفسه يعطيك كل ما يلي:
| الشاشة | النتيجة |
|---|---|
| iPhone Duo مطوي | القائمة، ثم الرسالة بملء الشاشة عند لمسها، وشريط التنقل بجانب الجزيرة |
| iPhone Duo مفتوح | القائمة والرسالة جنباً إلى جنب، مقسومتان على خط الطي |
| العرض المقسّم | لوحة واحدة، وشريط التنقل على الحافة الخارجية للتطبيق |
| جهاز Android نصف مفتوح | اللوحتان مقسومتان على المفصل تماماً |
| iPhone | لوحة واحدة وشريط تنقل سفلي |
| iPad وسطح المكتب والويب | القائمة والرسالة جنباً إلى جنب مع شريط تنقل جانبي |
| تطبيق بالعربية أو العبرية | كل شيء معكوس، ويبقى التقسيم على خط الطي |
ويمكنك قراءة بيانات النافذة بنفسك في أي مكان:
final duo = context.duo; // يُعاد البناء عند الطي والتدوير والعرض المقسّم
if (duo.isExpanded) { /* لوحتان */ }
duo.mode; // DuoMode.openLandscape
duo.safe.right; // 59، جهة الجزيرة الديناميكية
duo.gridColumns(160); // عدد زوجي من الأعمدة حول خط الطي
لماذا بُنيت بهذه الطريقة #
هذه هي النسخة المختصرة. التفاصيل الكاملة مع البدائل التي استبعدناها موجودة في DESIGN.ar.md (English).
- الأحجام لا تُكبَّر أبداً. للشاشتين كثافة النقطة نفسها، فيكسب التخطيط لوحات إضافية لا عناصر أكبر.
- كل شيء يأتي من
MediaQuery، فلا يُعاد البناء إلا عندما تتغير النافذة. - Dart فقط، على كل المنصات بدءاً من Flutter 3.41.
- يُكتشف iPhone Duo من مقاسات شاشته الدقيقة على iOS، فلا يُخلط مع iPad أو Android.
- الحالة تبقى بعد كل طيّة لأن الحزمة تنقل عناصر الواجهة بدلاً من إعادة بنائها.
- مُختبرة جيداً: 97 اختباراً و9 اختبارات تكامل.
ماذا تحتوي الحزمة #
context.duo |
الوضع وعدد اللوحات وخط الطي والوضعية والمناطق الآمنة. |
DuoLayout + DuoKeep |
تخطيط للمطوي وآخر للمفتوح مع الحفاظ على الحالة. |
DuoSplit |
لوحتان على خط الطي، أو لوحة واحدة إن ضاقت المساحة. |
DuoListDetail |
قائمة وتفاصيل مثل تطبيق البريد. |
DuoNavigationScaffold |
شريط سفلي أو جانبي حيث يضعه iOS 27. |
DuoAvoidFold |
يُبعد أزرار التحكم عن خط الطي. |
DuoMedia |
يلائم فيديو 16:9 مع الشاشة بأقل قص. |
DuoSimulator + DuoPose |
كل الوضعيات على أي جهاز وفي الاختبارات. |
DuoDebugOverlay |
عرض حيّ للوضع والحجم وخط الطي. |
context.duo #
جدول الخصائص الكامل في القسم الإنجليزي. ولتغيير الحدود لجزء من الشجرة استخدم DuoScope(expandedWidth: 700, child: ...).
عناصر الواجهة #
تخطيطان وحالة واحدة. ينقل DuoKeep العنصر نفسه بين التخطيطين بدلاً من إعادة بنائه:
DuoLayout(
closed: (_) => DuoKeep(id: 'feed', child: const Feed()),
open: (_) => Row(children: [
Expanded(child: DuoKeep(id: 'feed', child: const Feed())),
const Expanded(child: Trends()),
]),
)
لوحات تتبع خط الطي:
DuoSplit(primary: const Player(), secondary: const UpNext())
أزرار تحكم تتجنب خط الطي:
Stack(children: [
const VideoPlayer(),
const DuoAvoidFold(
child: Align(alignment: Alignment.bottomCenter, child: Controls()),
),
])
فيديو يلائم الشاشة:
DuoMedia(aspectRatio: 16 / 9, child: player) // smart: يقص حتى 15% وإلا أضاف أشرطة
من اليمين إلى اليسار ومن اليسار إلى اليمين #
كل شيء يتبع Directionality في تطبيقك، فتحصل تطبيقات العربية والعبرية والفارسية على تخطيطات معكوسة دون أي كود إضافي. وعندما تتبادل اللوحتان مكانيهما، يبقى التقسيم على خط الطي الفعلي تماماً.
| تطبيق من اليسار إلى اليمين | تطبيق من اليمين إلى اليسار | |
|---|---|---|
DuoSplit وDuoListDetail |
اللوحة الأساسية (القائمة) على اليسار | اللوحة الأساسية (القائمة) على اليمين |
DuoAvoidFold في وضع الكتاب |
النصف الأيمن | النصف الأيسر |
DuoSplit(ratio: .3) |
اللوحة الأساسية تأخذ 30% من اليسار | اللوحة الأساسية تأخذ 30% من اليمين |
وتختار أنت مكان شريط التنقل عبر railSide:
railSide |
iPhone Duo | الأجهزة الأخرى |
|---|---|---|
auto (الافتراضي) |
اليمين بجانب الجزيرة الديناميكية، مثل أشرطة iOS 27 العمودية في كل اللغات | جهة البداية: اليسار في الإنجليزية، واليمين في العربية |
start |
جهة البداية | جهة البداية |
end |
جهة النهاية | جهة النهاية |
left وright |
تلك الجهة دائماً | تلك الجهة دائماً |
DuoNavigationScaffold(railSide: DuoRailSide.start, ...) // اتبع اتجاه القراءة في كل مكان
DuoListDetail(textDirection: TextDirection.ltr, ...) // ثبّت تخطيطاً واحداً مهما كانت لغة التطبيق
اختبر كل الوضعيات دون جهاز #
testWidgets('unfolding keeps the draft', (tester) async {
await tester.pumpWidget(
const DuoSimulator(pose: DuoPose.closedPortrait, child: MyApp()),
);
await tester.enterText(find.byType(TextField), 'draft');
await tester.pumpWidget(
const DuoSimulator(pose: DuoPose.openLandscape, child: MyApp()),
);
expect(find.text('draft'), findsOneWidget);
});
الوضعيات الجاهزة: closedPortrait وclosedLandscape وopenLandscape وopenPortrait وsplitLeft وsplitRight وfoldableBook وfoldableTabletop وiPhone وiPad. ويمكنك إنشاء وضعياتك الخاصة عبر DuoPose(...).
ولرؤية القيم الحية أثناء التطوير:
MaterialApp(
builder: (context, child) =>
DuoDebugOverlay(enabled: kDebugMode, child: child!),
)
قبل النشر على iPhone Duo #
- ابنِ بـ Xcode 27.1، وإلا عمل تطبيقك في إطار ضيق على الشاشة الداخلية.
- اعتمد UIScene (الدليل).
- لا تعتمد على قفل الاتجاه.
- اقرأ
context.duoداخلbuildولا تخزّن الأحجام. - الجزيرة الديناميكية على جانب واحد، فعامل كل حافة على حدة.
- أوقف الوسائط عند
pausedلاinactive. - أبعد عناصر اللمس عن خط الطي.
iPhone Duo في لمحة #
| الشاشة الخارجية | الشاشة الداخلية | |
|---|---|---|
| القطر | 5.4 بوصة | 7.6 بوصة |
| اللوحة | 1398 × 2034 بكسل، 460 بكسل في البوصة | 1878 × 2670 بكسل، 430 بكسل في البوصة |
| النقاط (ما يراه Flutter) | 466 × 678 بكثافة 3x | 669 × 951 بكثافة 3x، تُرسم بدقة 2007 × 2853 ثم تُصغَّر إلى اللوحة |
| فئة الحجم | ضيقة (compact) | عادية × عادية (regular) |
القيود الحالية #
- لا زاوية مفصل على iOS بعد. Flutter لا يمرّرها إلى Dart (flutter#192515)، فتكون
postureإماclosedأوunknown. ستلتقطها الحزمة تلقائياً عندما تصل. وللزاوية الحية اليوم استخدمfoldable. - المناطق الآمنة في المحاكاة تقديرية (59 و21 نقطة). الأجهزة الحقيقية تستخدم القيم الحية.
- جهة التطبيق في العرض المقسّم تُستنتج من الجهة التي تحمل حافة الجزيرة الديناميكية.
- لا يوجد في Flutter مقابل للأشرطة العمودية الأصلية. يحاكي
DuoNavigationScaffoldأماكنها باستخدام عناصر Material.
التطبيق التجريبي #
cd example
flutter run # العرض الكامل مع أداة اختيار الوضعيات
flutter run -t lib/minimal.dart # التطبيق ذو الأربعين سطراً أعلاه
في العرض الكامل، اختر أي وضعية من شريط الخيارات لمحاكاتها على جهازك الحالي، أو فعّل RTL لرؤية النسخة العربية، أو اختر This device لتشغيله على جهازك فعلياً.
المساهمة #
نرحب بالإبلاغ عن الأخطاء وطلبات الدمج في صفحة المشكلات. وإذا لاحظت شيئاً غير صحيح على جهاز iPhone Duo حقيقي، فإن لقطة شاشة مع تفعيل DuoDebugOverlay ستساعد كثيراً.
المطوّر #
طوّر هذه الحزمة ويشرف عليها @Abojawdat.
- الحساب: github.com/Abojawdat
- الشيفرة المصدرية: github.com/Abojawdat/iPhone-Duo-package-
إذا أفادتك الحزمة، فإن إضافة نجمة للمستودع هي أفضل طريقة لتقول شكراً.
↑ العودة إلى الأعلى · Read in English
MIT © @Abojawdat · Source on GitHub
iPhone and iPhone Duo are trademarks of Apple Inc. This package isn't affiliated with or endorsed by Apple.