UI patterns, responsive layouts, master-detail experiences, adaptive navigation, and practical Flutter code examples.
Category: Flutter Development · Responsive UI · Mobile Architecture
Reading time: 12–15 minutes
Mobile interfaces are no longer limited to a single, fixed-size screen. Foldable devices, tablets, resizable application windows, and emerging multi-screen concepts create new opportunities to rethink how people interact with mobile applications.
Imagine an Apple Duo-style device concept where one screen displays a list of conversations while the other displays the selected conversation. Instead of repeatedly navigating between pages, users can view related information side by side, compare content, and complete tasks with fewer interactions.
Flutter provides flexible layout tools for building these experiences from a shared codebase. In this tutorial, we will explore dual-screen design patterns, responsive breakpoints, master-detail navigation, state preservation, and production-oriented implementation techniques.
“Apple Duo” is used here as a conceptual name for a dual-screen Apple-style experience, not as a claim about an officially announced Apple product. The tutorial implements adaptive Flutter layouts. It does not depend on a particular unreleased device or guarantee access to two independent physical displays.
What You Will Learn
- How to design applications for two-pane experiences.
- How to implement responsive layouts with LayoutBuilder.
- When to use master-detail, companion-pane, and continuous-canvas patterns.
- How to preserve the selected item when switching layouts.
- How to adapt navigation for narrow and wide windows.
- How to test, optimize, and prepare the application for production.
Table of Contents
- Understanding dual-screen design
- Three essential UI patterns
- Application architecture
- Responsive breakpoints
- Setting up the Flutter project
- Implementing the dual-pane interface
- Managing selection and navigation state
- Hinges, folds, and display features
- Accessibility and interaction design
- Testing and performance
- Production checklist
- Frequently asked questions
1. Understanding Dual-Screen App Design
A dual-screen experience divides an application into coordinated areas that serve different but related purposes. The two areas may appear side by side on a wide display, on separate physical screens, or as a split layout on a foldable device.
The goal is not simply to display more content. The goal is to make the relationship between content clearer and reduce unnecessary navigation.
Conceptual Dual-Screen Layout
Illustrative UI concept — not a rendering of an actual Apple device.
Why does this matter?
- Less navigation: Related content stays visible together.
- Better context: Users can inspect an item without losing the surrounding list.
- More workspace: Complex workflows can use the available width effectively.
- Flexible experiences: The same application can work across phones, foldables, tablets, and desktop windows.
2. Three Essential Dual-Screen UI Patterns
Choose a layout pattern based on the task the user is trying to complete, rather than the hardware alone.
Pattern A: Master-Detail Layout
Lists, emails, products, contacts
Selected item, content, actions
This is the best starting point for most dual-pane applications. One pane displays selectable items, and the other shows the selected item's details.
Best for: Email clients, messaging applications, product catalogs, file browsers, banking dashboards, and admin tools.
Pattern B: Companion Pane
Document or form
Help, preview, notes
The main pane contains the user's primary activity, while the companion pane provides relevant information without interrupting the workflow.
Best for: Learning applications, form-heavy business apps, document editors, and customer support tools.
Pattern C: Continuous Canvas
Timeline, drawing surface, planner, or board
Here, the content behaves as one workspace instead of two separate pages. The application must carefully account for any hinge, gap, or area that is not useful for displaying content.
Best for: Drawing tools, calendars, project boards, timelines, and creative applications.
Implement master-detail first. It has a clear selection model, adapts naturally to a narrow screen, and demonstrates the key engineering decisions behind dual-pane layouts.
3. Architecture for an Adaptive Flutter App
A maintainable implementation separates application state from layout decisions. The same selected item should be available whether the interface displays one pane or two.
Selected item · Current route · Data
Available window constraints
Single-pane navigation
Master + detail
For a small demonstration, a StatefulWidget can manage selection. In a larger app, Riverpod, BLoC, or another state-management solution can expose the same state to multiple widgets.
4. Choose Responsive Breakpoints
A common mistake is checking whether the device is a phone, tablet, or foldable. A more robust strategy is to check how much width the current application window actually has.
| Available width | Suggested layout | Navigation |
|---|---|---|
| Below 600 logical px | Single pane | Push detail route |
| 600–899 logical px | Compact two-pane or selected detail | Rail or compact navigation |
| 900 px and above | Full master-detail layout | Persistent navigation |
These are starting values, not universal rules. Test your actual content, minimum pane widths, text scaling, and user workflows before finalizing breakpoints.
5. Set Up the Flutter Project
Create a new project:
flutter create duo_adaptive_demo
cd duo_adaptive_demo
flutter run
This demonstration uses built-in Flutter widgets and does not require an external dual-screen package.
6. Implement the Adaptive Master-Detail Interface
The following complete example demonstrates a selectable list and a detail pane. On narrow windows, the detail screen opens as a separate route. On wider windows, the list and detail appear side by side.
Replace lib/main.dart with the following code:
import 'package:flutter/material.dart';
void main() {
runApp(const DuoDemoApp());
}
class DuoDemoApp extends StatelessWidget {
const DuoDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Adaptive Workspace',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF4059B8),
),
useMaterial3: true,
),
home: const WorkspaceScreen(),
);
}
}
class Message {
const Message({
required this.id,
required this.name,
required this.subject,
required this.preview,
required this.body,
required this.color,
});
final int id;
final String name;
final String subject;
final String preview;
final String body;
final Color color;
}
const messages = [
Message(
id: 1,
name: 'Alex Morgan',
subject: 'Design review',
preview: 'Can we review the latest prototype?',
body:
'Hi! I have prepared the latest prototype. '
'Can we review the layout, navigation, and '
'responsive behavior together today?',
color: Color(0xFF4776D0),
),
Message(
id: 2,
name: 'Sarah Chen',
subject: 'Project timeline',
preview: 'The updated timeline is ready.',
body:
'The updated project timeline is ready. '
'I have included the implementation tasks, '
'testing milestones, and release checklist.',
color: Color(0xFF168B80),
),
Message(
id: 3,
name: 'Jordan Lee',
subject: 'Flutter architecture',
preview: 'Here are the architecture notes.',
body:
'I have documented the shared state model, '
'responsive breakpoints, and widget boundaries '
'for the next development iteration.',
color: Color(0xFF9258B5),
),
];
class WorkspaceScreen extends StatefulWidget {
const WorkspaceScreen({super.key});
@override
State<WorkspaceScreen> createState() =>
_WorkspaceScreenState();
}
class _WorkspaceScreenState extends State<WorkspaceScreen> {
int selectedId = messages.first.id;
Message get selectedMessage =>
messages.firstWhere((item) => item.id == selectedId);
void selectMessage(Message message) {
setState(() {
selectedId = message.id;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Adaptive Workspace'),
),
body: LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth >= 900;
if (isWide) {
return Row(
children: [
SizedBox(
width: 320,
child: MessageList(
selectedId: selectedId,
onSelected: selectMessage,
),
),
const VerticalDivider(
width: 1,
thickness: 1,
),
Expanded(
child: MessageDetail(
message: selectedMessage,
),
),
],
);
}
return MessageList(
selectedId: selectedId,
onSelected: (message) {
selectMessage(message);
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => MessageDetailPage(
message: message,
),
),
);
},
);
},
),
);
}
}
class MessageList extends StatelessWidget {
const MessageList({
super.key,
required this.selectedId,
required this.onSelected,
});
final int selectedId;
final ValueChanged<Message> onSelected;
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
const Text(
'YOUR INBOX',
style: TextStyle(
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
const SizedBox(height: 12),
for (final message in messages)
Card(
color: message.id == selectedId
? Theme.of(context)
.colorScheme
.primaryContainer
: null,
child: ListTile(
leading: CircleAvatar(
backgroundColor: message.color,
child: Text(
message.name[0],
style: const TextStyle(color: Colors.white),
),
),
title: Text(
message.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
message.subject,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => onSelected(message),
),
),
],
);
}
}
class MessageDetailPage extends StatelessWidget {
const MessageDetailPage({
super.key,
required this.message,
});
final Message message;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Message details')),
body: MessageDetail(message: message),
);
}
}
class MessageDetail extends StatelessWidget {
const MessageDetail({
super.key,
required this.message,
});
final Message message;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(28),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 760),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 28,
backgroundColor: message.color,
child: Text(
message.name[0],
style: const TextStyle(
color: Colors.white,
fontSize: 24,
),
),
),
const SizedBox(height: 20),
Text(
message.subject,
style: Theme.of(context)
.textTheme
.headlineMedium,
),
const SizedBox(height: 8),
Text(
'From ${message.name}',
style: Theme.of(context).textTheme.titleMedium,
),
const Divider(height: 36),
Text(
message.body,
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(height: 1.8),
),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.reply),
label: const Text('Reply'),
),
],
),
),
),
);
}
}
How the implementation works
- LayoutBuilder: Reads the width available to the workspace.
- Breakpoint: Uses 900 logical pixels as the example threshold for a full two-pane layout.
- Shared selection: The parent widget owns the selected message ID.
- Wide layout: The list remains visible beside the selected message.
- Narrow layout: Selecting a message pushes a detail route onto the navigation stack.
- Reusable widgets: The list and detail components are shared between layouts.
Important implementation detail: This is an adaptive two-pane demo, not a physical-display detector. It responds to the width of the app window. If you need to account for a real hinge or multiple physical displays, integrate and test the relevant platform-specific capabilities separately.
7. Preserve State When the Layout Changes
One of the most important details in an adaptive application is keeping the user's context when the window changes size or the device folds and unfolds.
Imagine that a user selects a message in the wide layout. When the application becomes narrow, the selected message should remain the active item rather than unexpectedly returning to the first message.
For a more advanced app, keep the selected item ID in a state-management layer such as Riverpod or BLoC. The layout should read this state instead of independently maintaining its own selection.
// Conceptual state shared by both layouts.
class WorkspaceState {
const WorkspaceState({
required this.selectedMessageId,
});
final int selectedMessageId;
}
In a real application, selection changes, route updates, deep links, and restored state should all update the same source of truth.
State preservation checklist
- Preserve the selected record when the breakpoint changes.
- Retain list scroll position when appropriate.
- Keep unsaved form input when moving between layouts.
- Restore the correct detail screen after app lifecycle changes.
- Avoid duplicating the same business state in multiple widgets.
8. Handle Hinges, Folds, and Display Features
A wide window and a true dual-screen device are not identical. A device may contain a hinge, a fold, a gap, or two independent display surfaces. Important content should not be positioned blindly across an obstructed region.
Flutter exposes display-feature information through MediaQuery on supported Android configurations. The DisplayFeatureSubScreen widget can position content in a sub-screen, while the third-party dual_screen package offers a TwoPane widget for supported scenarios.
For example, the following diagnostic widget lists the display features reported to the current Flutter view:
import 'package:flutter/material.dart';
class DisplayFeatureDebugView extends StatelessWidget {
const DisplayFeatureDebugView({super.key});
@override
Widget build(BuildContext context) {
final features = MediaQuery.of(context).displayFeatures;
return ListView(
padding: const EdgeInsets.all(16),
children: [
Text('Display features: ${features.length}'),
for (final feature in features)
ListTile(
title: Text(feature.type.toString()),
subtitle: Text(
'Bounds: ${feature.bounds}',
),
),
],
);
}
}
This is a diagnostic example, not a complete hinge-aware layout. It displays features reported by the platform but does not automatically divide your interface into panes.
Flutter's documented display-feature support is platform-dependent; the framework API does not guarantee hinge information for a hypothetical Apple Duo. For an actual Apple device, use the officially supported iOS/iPadOS window and display APIs available to your target OS, and verify behavior on real hardware when it becomes available.
9. Accessibility and Interaction Design
Additional screen space should make the application easier to use, not merely more visually complex.
- Readable text: Support dynamic text scaling without clipping important controls.
- Clear selection: Use more than color alone to indicate the active item.
- Keyboard navigation: Support focus traversal and standard keyboard interactions where relevant.
- Touch targets: Keep interactive elements comfortably sized and spaced.
- Predictable navigation: Provide a clear back action on narrow layouts.
- Meaningful semantics: Give icons and custom controls appropriate labels.
- RTL support: Ensure pane ordering and navigation work correctly for right-to-left languages.
10. Testing Dual-Screen and Adaptive Layouts
Do not rely on a single emulator size. A responsive application should be tested across multiple window widths, orientations, input types, and state transitions.
| Test scenario | Expected result |
|---|---|
| Narrow app window | List and detail navigation work without overflow. |
| Wide app window | Both panes appear and selection updates the detail. |
| Resize across breakpoint | The selected item remains consistent. |
| Long text and large fonts | Content remains readable without clipping. |
| Keyboard and screen reader | Focus, labels, and navigation are understandable. |
| Fold or unfold on supported hardware | Layout and application state adjust without losing context. |
Test the responsive breakpoint
Flutter widget tests can verify that the layout changes when the available width crosses the chosen threshold. For example, pump the workspace at 500 logical pixels and 1,000 logical pixels, then assert that the expected navigation and detail elements appear.
For actual hinge behavior, use a compatible emulator or supported physical device. A desktop browser preview cannot reproduce every hardware posture or platform-specific display feature.
11. Production Readiness Checklist
Layout and navigation
- Use constraints rather than hardcoded device types.
- Define minimum useful widths for each pane.
- Provide a sensible single-pane fallback.
- Keep navigation behavior predictable.
State and data
- Maintain one source of truth for selected content.
- Preserve unsaved edits and scroll positions where necessary.
- Handle loading, empty, and error states.
- Support deep links and state restoration when required.
Quality and compatibility
- Test different screen sizes and accessibility settings.
- Validate any device-specific API against official documentation.
- Profile large lists and expensive rebuilds.
- Test on target hardware before claiming device-specific support.
12. Frequently Asked Questions
Can Flutter build dual-screen applications?
Yes. Flutter can build adaptive layouts with multiple panes and can support foldable experiences using available platform capabilities. The exact behavior depends on the device, operating system, and APIs involved.
Is Apple Duo an officially released Apple device?
This article treats Apple Duo as a conceptual dual-screen design target, not a verified commercial product. Confirm official Apple announcements and developer documentation before making claims about a particular device.
Should I use Row or LayoutBuilder for dual-pane layouts?
They solve different problems. Row can arrange panes horizontally; LayoutBuilder lets you decide which layout to use based on available space. A typical adaptive design uses LayoutBuilder to select between a Row-based two-pane layout and a single-pane layout.
How do I detect a fold or hinge in Flutter?
On supported Android configurations, inspect display features from MediaQuery. For advanced foldable behavior, review the current Flutter APIs and relevant platform packages. Do not assume identical hinge-detection capabilities on every operating system.
What is the best UI pattern for a dual-screen app?
Master-detail is a strong default for applications with selectable records and associated details. Companion-pane and continuous-canvas layouts are better suited to workflows where secondary information or a shared workspace is central to the task.
How can I test dual-pane layouts without the target device?
Use Flutter widget tests and resizeable emulator windows to verify responsive behavior. Hardware-specific posture, hinge, and independent-display features still require appropriate platform emulation or physical-device testing.
Conclusion
Dual-screen design is an opportunity to rethink how users move through information, not simply a reason to increase the size of an existing interface.
By combining responsive breakpoints, reusable widgets, shared application state, and appropriate master-detail patterns, Flutter developers can build experiences that work across narrow mobile windows and larger multi-pane environments.
Start with a simple adaptive layout, preserve the user's context as the window changes, and add platform-specific hinge or display support only when the target hardware and APIs are confirmed.
Building your next adaptive Flutter app?
Explore the official Flutter resources below and experiment with responsive layouts in your own project.
Read Flutter Adaptive UI DocsReferences and Further Reading
- Flutter: Adaptive and responsive design
- Flutter: Large screen devices
- Flutter: Adaptive design best practices
- Flutter API: DisplayFeatureSubScreen
- Flutter Dual Screen package
Technical note: The code is an illustrative adaptive-layout example. Validate it with your target Flutter SDK, operating systems, and hardware before using it in production. Device-specific functionality should be verified against current official documentation.
Comments
Post a Comment