Building Adaptive Flutter Apps for Foldable and Dual-Screen Devices

Building Adaptive Flutter Apps for Foldable and Dual-Screen Devices

Design flexible mobile experiences that adapt to screen size, display features, folding posture, and available space using Flutter.

Flutter Dart Responsive UI Foldables

Reading time: 12–15 minutes | Level: Intermediate to Advanced

Mobile applications are no longer limited to traditional rectangular smartphone screens. Foldable phones, devices with flexible displays, and dual-screen hardware introduce new opportunities for multitasking, productivity, media consumption, and richer user experiences.

For Flutter developers, these devices introduce an important challenge: an application should not simply stretch its existing interface across a larger display. It should understand the available space, respond to display features such as hinges, and adapt its layout to the user's current device configuration.

In this guide, we will explore how to build adaptive Flutter applications using responsive layout techniques, the MediaQuery and LayoutBuilder APIs, and display-feature information exposed by Flutter.

What You Will Learn

  • Understand foldable and dual-screen device form factors.
  • Choose between responsive and adaptive UI design.
  • Read screen dimensions and display features in Flutter.
  • Build single-pane and two-pane layouts.
  • Handle hinge-aware layouts and changing window sizes.
  • Test adaptive interfaces across device configurations.
  • Apply production-level accessibility and performance practices.

1. Introduction to Adaptive Flutter Apps

Traditional mobile interfaces are usually designed around a single viewport. Foldable devices complicate this assumption because the usable display area can change significantly when the device is opened, closed, or partially folded.

A compact folded screen might require a familiar single-column interface. An expanded screen could provide enough room for a navigation panel and a detailed content area. A device with a physical hinge might require the application to keep important content away from the hinge region.

Flutter provides flexible layout widgets and display-feature information that help developers build interfaces for these scenarios without maintaining an entirely separate application for every device type.

Core principle

Build around the available window size and display configuration, not around assumptions about a specific phone model.

2. Understanding Foldable and Dual-Screen Devices

Before choosing a layout strategy, understand the major form factors and how each affects the usable display area.

Traditional Smartphone

A compact single-screen layout with vertically arranged content.

Book-Style Foldable

An expanded inner screen that can support two-pane layouts.

Clamshell Foldable

A compact folded screen and a larger unfolded display.

Dual-Screen Device

Two separate display regions that may be divided by a hinge or gap.

Not every foldable device exposes the same display features. Some have a continuous flexible panel, while others have separate panels with a physical hinge. Your implementation should use actual window dimensions and display-feature data rather than assuming that every expanded screen has a hinge.

3. Responsive Design vs Adaptive Design

These concepts are related but serve different purposes.

Responsive Design Adaptive Design
Adjusts layout to available space. Selects a suitable layout or interaction for a configuration.
Uses flexible widgets and constraints. May use different navigation, pane, or interaction patterns.
Example: a grid changes its column count. Example: a list opens details in a second pane.

A strong foldable application combines both approaches. It uses responsive constraints to fit the current window and adaptive logic to decide whether the user should see one panel or two.

4. Flutter APIs for Adaptive Layouts

MediaQuery

MediaQuery provides information about the current Flutter window, including its size, text scaling, padding, and reported display features.

final media = MediaQuery.of(context);

final width = media.size.width;
final height = media.size.height;
final displayFeatures = media.displayFeatures;

Use this information when you need window-level details. Remember that the app window size can differ from the physical screen size, especially in multi-window or resizable environments.

LayoutBuilder

LayoutBuilder exposes the constraints available to a particular part of your widget tree. It is useful for selecting a single-pane or two-pane layout based on actual available width.

LayoutBuilder(
  builder: (context, constraints) {
    if (constraints.maxWidth >= 700) {
      return const Text('Expanded layout');
    }

    return const Text('Compact layout');
  },
)

The value of 700 pixels is an example breakpoint, not a universal foldable-device standard. Choose breakpoints based on your content and test them across the window sizes your app supports.

DisplayFeature

Flutter's display-feature information can describe a hinge, fold, or cutout within the display. A feature's bounds and type help determine whether part of the window should be avoided or treated as a divider.

final features =
    MediaQuery.of(context).displayFeatures;

for (final feature in features) {
  debugPrint('Type: ${feature.type}');
  debugPrint('Bounds: ${feature.bounds}');
}

A reported display feature is not necessarily a separating hinge. Interpret the feature type, bounds, and current layout constraints before deciding how the UI should respond.

5. Adaptive Application Architecture

Keep layout decisions separate from business logic. This makes it easier to maintain the application as new form factors are introduced.

Application State and Business Logic
↓
Adaptive Presentation Layer
↙↓↘
Compact
Single Pane
Expanded
Two Pane
Hinge-aware
Separated Areas

In a larger project, the presentation layer can use Riverpod, BLoC, or another state-management approach. The adaptive layout should consume application state rather than duplicate data-fetching logic for each form factor.

6. Build a Practical Adaptive Flutter Application

Let's create a simple article browser. On compact windows, users will see a list of articles. On wider windows, the list and article details will appear side by side.

This is a useful pattern for news readers, email applications, dashboards, document browsers, and productivity tools.

Step 1: Create the Flutter project

flutter create adaptive_flutter_demo
cd adaptive_flutter_demo
flutter run

Step 2: Create a reusable adaptive layout

For this example, replace lib/main.dart with the following code. It uses only Flutter's built-in Material widgets and supports compact and expanded layouts.

import 'package:flutter/material.dart';

void main() {
  runApp(const AdaptiveDemoApp());
}

class AdaptiveDemoApp extends StatelessWidget {
  const AdaptiveDemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Adaptive Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.indigo,
        ),
        useMaterial3: true,
      ),
      home: const ArticlesPage(),
    );
  }
}

class Article {
  const Article({
    required this.title,
    required this.description,
  });

  final String title;
  final String description;
}

const articles = [
  Article(
    title: 'Adaptive Layouts',
    description:
        'Build interfaces that respond to available window constraints.',
  ),
  Article(
    title: 'Foldable Devices',
    description:
        'Design for changing display areas and hinge-aware layouts.',
  ),
  Article(
    title: 'Flutter Performance',
    description:
        'Keep rendering smooth across different form factors.',
  ),
];

class ArticlesPage extends StatefulWidget {
  const ArticlesPage({super.key});

  @override
  State createState() => _ArticlesPageState();
}

class _ArticlesPageState extends State {
  int selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Adaptive Articles'),
      ),
      body: LayoutBuilder(
        builder: (context, constraints) {
          final isExpanded = constraints.maxWidth >= 700;

          if (isExpanded) {
            return Row(
              children: [
                SizedBox(
                  width: 300,
                  child: ArticleList(
                    selectedIndex: selectedIndex,
                    onSelected: (index) {
                      setState(() {
                        selectedIndex = index;
                      });
                    },
                  ),
                ),
                const VerticalDivider(width: 1),
                Expanded(
                  child: ArticleDetails(
                    article: articles[selectedIndex],
                  ),
                ),
              ],
            );
          }

          return ArticleList(
            selectedIndex: selectedIndex,
            onSelected: (index) {
              Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (_) => ArticleDetailsPage(
                    article: articles[index],
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

class ArticleList extends StatelessWidget {
  const ArticleList({
    super.key,
    required this.selectedIndex,
    required this.onSelected,
  });

  final int selectedIndex;
  final ValueChanged onSelected;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: articles.length,
      itemBuilder: (context, index) {
        final article = articles[index];

        return ListTile(
          selected: index == selectedIndex,
          leading: CircleAvatar(
            child: Text('${index + 1}'),
          ),
          title: Text(article.title),
          subtitle: Text(
            article.description,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
          ),
          onTap: () => onSelected(index),
        );
      },
    );
  }
}

class ArticleDetails extends StatelessWidget {
  const ArticleDetails({
    super.key,
    required this.article,
  });

  final Article article;

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            article.title,
            style: Theme.of(context)
                .textTheme
                .headlineMedium,
          ),
          const SizedBox(height: 16),
          Text(
            article.description,
            style: Theme.of(context).textTheme.bodyLarge,
          ),
        ],
      ),
    );
  }
}

class ArticleDetailsPage extends StatelessWidget {
  const ArticleDetailsPage({
    super.key,
    required this.article,
  });

  final Article article;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Article Details'),
      ),
      body: ArticleDetails(article: article),
    );
  }
}

Step 3: Understand the layout decision

The key logic is the width check inside LayoutBuilder:

final isExpanded = constraints.maxWidth >= 700;

if (isExpanded) {
  // Show list and details together.
} else {
  // Show a compact list and open details separately.
}

On a wide window, the list and detail panel appear side by side. On a narrow window, selecting an article opens a dedicated detail page.

This is a basic adaptive master-detail pattern. It does not assume that the expanded window is necessarily a foldable device; the same layout can be useful on tablets, desktop windows, and resizable application windows.

Implementation note

The example intentionally uses a simple layout breakpoint. A production foldable application should additionally account for display-feature bounds, safe areas, accessibility, and the user's current navigation state.

7. Handling Hinges and Display Features

A wide window does not always provide one uninterrupted content area. A hinge or fold can occupy part of the display, making it important to position content carefully.

Flutter exposes display features through MediaQuery.of(context).displayFeatures. You can inspect each feature's type and bounds to decide how your application should arrange its content.

Read display-feature information

import 'package:flutter/material.dart';

void logDisplayFeatures(BuildContext context) {
  final features =
      MediaQuery.of(context).displayFeatures;

  for (final feature in features) {
    debugPrint('Feature type: ${feature.type}');
    debugPrint('Feature bounds: ${feature.bounds}');
    debugPrint('Feature state: ${feature.state}');
  }
}

This diagnostic example helps you inspect what Flutter reports on a target device or emulator. Display-feature information depends on the device, operating system, and available platform integration.

Example: Identify a vertical hinge or fold

The following helper identifies a reported vertical hinge or fold that spans most of the available window height. It returns the first matching feature; applications that support multiple features should evaluate all candidates.

import 'package:flutter/material.dart';

DisplayFeature? findVerticalHinge(
  BuildContext context,
) {
  final size = MediaQuery.sizeOf(context);
  final features =
      MediaQuery.of(context).displayFeatures;

  for (final feature in features) {
    final isHingeOrFold =
        feature.type == DisplayFeatureType.hinge ||
        feature.type == DisplayFeatureType.fold;

    final isVertical =
        feature.bounds.height >= size.height * 0.7 &&
        feature.bounds.width > 0;

    if (isHingeOrFold && isVertical) {
      return feature;
    }
  }

  return null;
}

This helper is a starting point, not a complete hinge-layout engine. In particular, it does not establish whether a feature actually separates two usable regions. Validate its bounds, state, and position relative to your content before changing the layout.

Hinge-aware layout strategy

Left region

Navigation or item list

Right region

Details or content

Conceptual illustration only. Actual hinge dimensions and display regions must come from the current device configuration.

For a robust implementation, determine the usable rectangles around the reported display feature and lay out content within those regions. Flutter's DisplayFeatureSubScreen can also help place a widget in the sub-screen nearest a chosen anchor point when its behavior fits your use case. Consult the API documentation for its exact behavior and limitations.

Do not simply add a fixed-width spacer at the center of every expanded screen. Some devices have no hinge, some report a fold that is not a separating gap, and some have display features in different positions.

8. Adapting to Device Posture and Window Changes

Foldable devices may be used in several configurations, including fully open, folded, or partially open. The appropriate layout depends on both the available space and the capabilities exposed by the device and operating system.

Flutter's core MediaQuery display-feature API provides information about reported display features. It should not be treated as a universal, cross-platform posture sensor. If your application needs explicit hinge-angle or device-posture information, evaluate an appropriate maintained platform plugin and verify support for your target devices.

Recommended layout behavior

  • Compact window: Use a single-pane interface and simple navigation.
  • Expanded window: Consider master-detail layouts, persistent navigation, or multi-column grids.
  • Separating hinge: Position related content in appropriate usable regions rather than underneath the hinge.
  • Window resizing: Recompute layout from current constraints instead of relying on the device's model name.
  • Configuration changes: Preserve selected items, scroll position, and user-entered state whenever practical.

A well-designed adaptive application should remain usable when the window changes size. Avoid resetting the entire screen or repeating network requests unnecessarily when only the layout needs to change.

9. Production Best Practices

01. Design for content, not devices

Choose breakpoints based on minimum content widths, navigation needs, and readable layouts rather than device brand or model.

02. Respect display features

Inspect hinge and fold bounds when available, and prevent critical controls or text from occupying obstructed regions.

03. Keep business logic independent

Use repositories, state-management solutions, and reusable widgets so layout changes do not duplicate business logic.

04. Preserve application state

Keep navigation selection, drafts, and scroll positions stable when switching between compact and expanded layouts.

05. Support accessibility

Use semantic controls, adequate touch targets, readable text, and predictable focus order across different layouts.

06. Optimize rendering

Use lazy lists for large datasets, avoid unnecessary rebuilds, and profile performance in realistic release configurations.

10. Testing Adaptive Flutter Applications

Testing only on a standard smartphone is not enough for an application that targets foldable and dual-screen devices.

Recommended test matrix

Scenario What to verify
Compact portrait Readable content, correct navigation, no overflow.
Expanded landscape Suitable column widths and balanced spacing.
Hinge or fold present Important controls and content avoid obstructed regions.
Resize or unfold Layout updates and application state is preserved.
Large text settings Text remains readable and controls remain accessible.
Split-screen or resized window Layout follows actual window constraints.

Useful testing techniques

  • Use widget tests with different surface sizes to verify breakpoint behavior.
  • Test reported display-feature data with suitable platform integration or test fixtures where supported.
  • Use Android emulators that support foldable configurations and verify results on physical hardware.
  • Check that resizing does not cause duplicate requests, lost user input, or unexpected navigation resets.
  • Profile scrolling, animations, and transitions on representative devices.

Emulators are useful for repeatable tests, but physical devices remain important for validating hinge behavior, touch interaction, system gestures, and device-specific platform integration.

11. Common Challenges and Solutions

Problem: The layout looks stretched on an expanded screen

Solution: Introduce content-aware breakpoints and use a multi-pane layout instead of allowing every component to grow indefinitely.

Problem: Content appears under a hinge

Solution: Inspect the reported display features and bounds. Where appropriate, position content within separate usable regions or use a suitable display-feature-aware widget.

Problem: The UI does not update after resizing

Solution: Ensure layout decisions depend on current constraints and inherited media information. Avoid caching screen dimensions in a permanent variable.

Problem: Switching layouts loses the selected item

Solution: Store selection and business state above layout-specific widgets, and reuse that state when rebuilding the presentation.

Problem: A breakpoint works on one device but not another

Solution: Derive breakpoints from the content's requirements. Consider split-screen mode, window resizing, text scaling, and the available space rather than relying only on physical screen dimensions.

12. Frequently Asked Questions

Can Flutter build apps for foldable devices?

Yes. Flutter supports responsive layouts and exposes display-feature information that can help applications adapt to foldable and other nontraditional displays. Some device-specific posture capabilities may require additional platform integration.

What is the difference between responsive and adaptive Flutter UI?

Responsive UI adjusts to available space, while adaptive UI chooses appropriate layouts or interactions for the current configuration. Most high-quality foldable applications benefit from both.

How can I detect a hinge in Flutter?

Inspect MediaQuery.of(context).displayFeatures and evaluate each feature's type, bounds, and state. Verify actual behavior on supported devices because reported features vary by platform and hardware.

Should I use a third-party foldable package?

Start with Flutter's built-in layout and display-feature APIs. If your application needs explicit device-posture information or specialized two-pane behavior, evaluate a maintained package that supports your target platforms and Flutter version.

Which breakpoint is best for a foldable application?

There is no universal breakpoint. A value such as 700 logical pixels can be a useful starting point, but the final choice should depend on your content, navigation, and minimum usable pane widths.

Can the same adaptive design work on tablets and desktops?

Yes. A content-driven layout can adapt to phones, foldables, tablets, desktops, and resizable windows without depending on a particular device model.

Conclusion: Build Once, Adapt Everywhere

Building adaptive Flutter applications for foldable and dual-screen devices requires more than stretching widgets across a larger screen. Developers must understand window constraints, display features, content hierarchy, navigation, and the importance of preserving application state.

By combining LayoutBuilder, MediaQuery, thoughtful layout breakpoints, and hinge-aware strategies, you can create applications that feel natural across a broad range of devices.

Create flexible interfaces. Respect the display. Deliver a better user experience.

Further Reading and Official Resources

Technical note: Flutter APIs and device behavior can change across releases and platforms. Verify the current stable SDK, supported display-feature behavior, and package compatibility before using device-specific capabilities in production.

Comments