FLUTTER • ADAPTIVE UI • CONCEPT DESIGN
Designing and Building Dual-Screen Apps for the Concept “iPhone Duo” in Flutter
Explore adaptive layouts, two-pane navigation, simulated dual-screen experiences, and the engineering decisions behind apps designed for two displays.
Reading time: 10–12 minutes
What if an iPhone could provide two displays that work together? Imagine reading an article on one screen while keeping notes on the other, browsing a product list beside its details, or viewing a video while controlling playback on a separate panel.
This kind of experience introduces an interesting app-design challenge: the interface must understand how much space is available, arrange content intelligently, and continue working when the app moves between compact and expanded layouts.
Flutter is well suited to this design problem because its layout system can adapt to different window sizes. In this tutorial, we will build the foundation of a dual-pane experience without relying on any unannounced device-specific API.
What you will learn
- How to plan a dual-screen user experience.
- How to switch between one-pane and two-pane layouts.
- How to build a master-detail interface in Flutter.
- How to simulate a second display during development.
- How to test responsive behavior and handle platform limitations.
1. Understanding the “iPhone Duo” Concept
For this article, imagine a device with two usable display areas. They might sit side by side, be separated by a hinge, or behave as separate surfaces connected by the operating system. The exact hardware is hypothetical, so the app architecture should not depend on a particular physical design.
The key design idea is to treat the available space as a flexible canvas. On a compact screen, show one primary task at a time. When enough space is available, show related content together.
Browse / Select
Details / Actions
Illustration: a conceptual two-panel experience, not a representation of an actual Apple device.
2. Design Principles for Dual-Screen Apps
Give each pane a clear purpose
A second display should provide meaningful value, not simply repeat the same interface. Useful pairings include:
- List and detail: select a message, product, contact, or article on one side and view its content on the other.
- Work and tools: write in one pane while keeping a reference, preview, or controls visible in another.
- Media and controls: show a video or presentation alongside playback or navigation controls.
- Map and information: keep a map visible while showing details about a selected location.
Avoid assuming a fixed orientation
A device may be held vertically or horizontally, and its available window can change due to multitasking, resizing, or system UI. Prefer constraints based on available width rather than hard-coded device models.
Plan for a gap or hinge
If a future device exposes hinge or occlusion information, the app should avoid placing important text, buttons, or input fields behind that area. The examples below use two panes inside one Flutter window; they do not claim to control two physical displays independently.
3. Build an Adaptive Layout with Flutter
The first step is to decide when the app should show one pane and when it should show two. Flutter's LayoutBuilder provides the constraints available to a widget at layout time.
For example, a product app might use a single-column layout below a chosen width and a two-column layout above it. The breakpoint is a design decision and should be tested with real content.
Simple responsive layout example
LayoutBuilder(
builder: (context, constraints) {
final useTwoPanes = constraints.maxWidth >= 700;
if (useTwoPanes) {
return Row(
children: [
Expanded(child: ProductListPane()),
const VerticalDivider(width: 1),
Expanded(child: ProductDetailsPane()),
],
);
}
return ProductListPane();
},
)
This is a layout pattern, not a physical-display detector. It uses the width Flutter receives from its parent. In a real app, you would also define what happens when a user selects an item on a compact screen.
4. Complete Example: Master-Detail Product Browser
The following example is a small, runnable Flutter app. On narrow windows, users see a list and can tap an item to open its details. On wider windows, the list and selected item's details appear side by side. This lets you test the core interaction today, even without a dual-screen device.
File: lib/main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const DuoConceptApp());
}
class Product {
const Product(this.name, this.description, this.icon);
final String name;
final String description;
final IconData icon;
}
const products = [
Product(
'Camera',
'Explore photography settings, lenses, and image tools.',
Icons.camera_alt_outlined,
),
Product(
'Notebook',
'Keep notes and ideas available while you browse.',
Icons.menu_book_outlined,
),
Product(
'Headphones',
'Review audio features and playback controls.',
Icons.headphones_outlined,
),
Product(
'Travel bag',
'Compare features, capacity, and travel essentials.',
Icons.work_outline,
),
];
class DuoConceptApp extends StatelessWidget {
const DuoConceptApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Duo Layout Concept',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF5B4BDB),
),
),
home: const ProductBrowserPage(),
);
}
}
class ProductBrowserPage extends StatefulWidget {
const ProductBrowserPage({super.key});
@override
State<ProductBrowserPage> createState() =>
_ProductBrowserPageState();
}
class _ProductBrowserPageState extends State<ProductBrowserPage> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Duo Concept'),
),
body: LayoutBuilder(
builder: (context, constraints) {
final wideLayout = constraints.maxWidth >= 700;
if (wideLayout) {
return Row(
children: [
SizedBox(
width: 280,
child: ProductList(
selectedIndex: _selectedIndex,
onSelected: (index) {
setState(() => _selectedIndex = index);
},
),
),
const VerticalDivider(width: 1),
Expanded(
child: ProductDetails(
product: products[_selectedIndex],
),
),
],
);
}
return ProductList(
selectedIndex: _selectedIndex,
onSelected: (index) {
setState(() => _selectedIndex = index);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ProductDetailsPage(
product: products[index],
),
),
);
},
);
},
),
);
}
}
class ProductList extends StatelessWidget {
const ProductList({
super.key,
required this.selectedIndex,
required this.onSelected,
});
final int selectedIndex;
final ValueChanged<int> onSelected;
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(12),
children: [
const Padding(
padding: EdgeInsets.all(12),
child: Text(
'Explore items',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
),
for (var i = 0; i < products.length; i++)
Card(
child: ListTile(
selected: i == selectedIndex,
leading: Icon(products[i].icon),
title: Text(products[i].name),
subtitle: const Text('Tap to view details'),
trailing: const Icon(Icons.chevron_right),
onTap: () => onSelected(i),
),
),
],
);
}
}
class ProductDetails extends StatelessWidget {
const ProductDetails({super.key, required this.product});
final Product product;
@override
Widget build(BuildContext context) {
return Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(32),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(product.icon, size: 76),
const SizedBox(height: 20),
Text(
product.name,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 12),
Text(
product.description,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.check_circle_outline),
label: const Text('Select item'),
),
],
),
),
),
);
}
}
class ProductDetailsPage extends StatelessWidget {
const ProductDetailsPage({super.key, required this.product});
final Product product;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Item details')),
body: ProductDetails(product: product),
);
}
}
5. How to Simulate a Dual-Screen Experience
You do not need hypothetical hardware to develop the layout. Start by testing your app in different window sizes:
- Run the app on an emulator or desktop target.
- Resize the window or use a larger emulator configuration.
- Verify that the app switches between one-pane and two-pane modes at the intended breakpoint.
- Test long titles, large text, keyboard visibility, and narrow layouts.
- Check that selection state is preserved when the layout changes.
You can also add a development-only preview mode that draws two panels inside one screen. This is useful for reviewing visual hierarchy, but it is only a simulation: it cannot test display routing, hinge sensors, or operating-system behavior for independent physical displays.
6. Manage State and Navigation
Dual-pane layouts work best when selection state is separate from the visual layout. In the sample, _selectedIndex represents the currently selected product. The same state drives the details pane in wide mode and the details route in compact mode.
For a production app, consider the following:
- Keep selected item IDs in a view model, state notifier, or other state-management layer if multiple widgets need them.
- Preserve scroll position when switching between layouts where practical.
- Use stable identifiers instead of relying only on list indexes when data can be reordered.
- Define back-button behavior for compact mode and selection behavior for wide mode.
- Keep business logic independent from the number of panes.
7. Native APIs and Two Physical Displays
A responsive Flutter layout is not the same as driving two separate physical displays. A real device would need operating-system support and a way for the application to learn about display topology, available display areas, and any hinge or occlusion regions.
Flutter's standard layout widgets do not automatically discover a hypothetical second iPhone display. If a future platform exposes additional capabilities through native APIs, developers may need a platform plugin or platform channel to access those APIs, subject to the operating system's public developer interfaces and app permissions.
Do not assume that a standard iOS app can place arbitrary Flutter content on a second physical display. Confirm the actual device capabilities, supported APIs, and distribution rules before designing a production feature around them.
8. Testing, Accessibility, and Performance
- Responsive tests: test compact, medium, and expanded window widths.
- Text scaling: verify that larger fonts do not overflow or make controls inaccessible.
- Keyboard and focus: ensure keyboard navigation and text input work across panes.
- Screen readers: give controls meaningful labels and preserve a logical reading order.
- State restoration: check whether the selected item and scroll positions should survive app lifecycle changes.
- Performance: avoid rebuilding large, unrelated sections when only the selected detail changes.
- Platform differences: test on real supported hardware when hardware-specific APIs become available.
Conclusion
Designing for a hypothetical “iPhone Duo” is a useful way to explore adaptive Flutter architecture. Start with a flexible master-detail layout, use available constraints to select the right presentation, keep selection state separate from the UI, and test carefully across screen sizes.
Most importantly, distinguish a simulated two-pane interface from true multi-display support. Responsive Flutter techniques are available today; independent physical-display behavior depends on the capabilities and public APIs of the actual device and operating system.
This article describes a conceptual device design and general Flutter techniques. It does not imply that Apple has announced an “iPhone Duo.”
Comments
Post a Comment