Responsive and adaptive layouts

Flutter · Practical · 12 min

Flutter apps run on phones, tablets, foldables, desktops and the web, so one fixed layout isn't enough.

Responsive means the layout adjusts to available space; adaptive means it adjusts to the device's input and conventions (mouse vs touch, iOS vs Android).

Tools for size:

  • MediaQuery.sizeOf(context) — the window size. Use the specific sizeOf, paddingOf, orientationOf methods rather than MediaQuery.of(context), so the widget only rebuilds when that aspect changes.
  • `LayoutBuilder` — gives the constraints from the parent, which is better for reusable components that may be placed in a side panel rather than full screen.
  • Breakpoints, following Material 3 window size classes: compact below 600 dp wide, medium 600–839, expanded 840 and up. A compact layout might use NavigationBar at the bottom; wider ones a NavigationRail and two panes.

Other essentials:

  • `SafeArea` keeps content clear of notches, status bars and home indicators.
  • Flexible widgets (Expanded, Wrap, GridView with maxCrossAxisExtent) adapt better than fixed sizes.
  • OrientationBuilder or the size itself for rotation.
  • Text scaling: test with large fonts; don't fix heights around text.

Adaptive touches: Theme.of(context).platform or defaultTargetPlatform to choose platform-specific behavior, .adaptive constructors (Switch.adaptive, CircularProgressIndicator.adaptive), hover and keyboard shortcuts on desktop and web.

Don't lock the app to portrait unless there's a strong reason; Android large-screen guidance discourages it.

Example

import 'package:flutter/material.dart';
class AdaptiveShell extends StatelessWidget {
  const AdaptiveShell({super.key, required this.index, required this.onSelect, required this.body});
  final int index;
  final ValueChanged<int> onSelect;
  final Widget body;
  static const _dests = [(Icons.inbox, 'Inbox'), (Icons.settings, 'Settings')];

  @override
  Widget build(BuildContext context) {
    final wide = MediaQuery.sizeOf(context).width >= 600;   // compact < 600
    if (!wide) {
      return Scaffold(body: SafeArea(child: body), bottomNavigationBar: NavigationBar(
        selectedIndex: index, onDestinationSelected: onSelect,
        destinations: [for (final (i, l) in _dests) NavigationDestination(icon: Icon(i), label: l)]));
    }
    return Scaffold(body: SafeArea(child: Row(children: [
      NavigationRail(selectedIndex: index, onDestinationSelected: onSelect,
          labelType: NavigationRailLabelType.all,
          destinations: [for (final (i, l) in _dests) NavigationRailDestination(icon: Icon(i), label: Text(l))]),
      const VerticalDivider(width: 1),
      Expanded(child: body),
    ])));
  }
}

Remember

  • Use MediaQuery.sizeOf for window size and LayoutBuilder for local space
  • Switch layouts at breakpoints (e.g. 600 and 840 dp)
  • Wrap content in SafeArea and test large text

Watch out for

  • Using MediaQuery.of(context).size everywhere, causing rebuilds on any media change such as the keyboard opening.

Check yourself

  1. A reusable card should pick a layout based on the space its parent gives it, not the whole screen. What should it use?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.flutter.dev/ui/adaptive-responsive

Builds on

Unlocks

Nothing builds on this yet.