Kodokon kodokon.com

Adaptive: LayoutBuilder, MediaQuery and platforms

Build interfaces that adapt to the parent's constraints, to the window and to each OS's conventions.

9 min · 3 questions

Open this lesson in Kodokon

MediaQuery describes the logical window, not the physical screen: size, padding (notch, system bar), viewInsets (keyboard), textScaler... A classic pitfall: MediaQuery.of(context) subscribes the widget to all these properties. When the keyboard opens, viewInsets changes and every subscribed widget rebuilds - including those that only read the width. Since Flutter 3.10, MediaQueryData is exposed via an InheritedModel: the targeted accessors MediaQuery.sizeOf(context), paddingOf, viewInsetsOf... subscribe the widget only to the requested aspect. Expert reflex: banish .of(context).size from your codebase.

DART
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.sizeOf(context).width;
    final columns = switch (width) {
      >= 900 => 4,
      >= 600 => 3,
      _ => 2,
    };
    return GridView.builder(
      gridDelegate:
          SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: columns,
      ),
      itemCount: 24,
      itemBuilder: (context, i) => Card(
        child: Center(child: Text('Tile $i')),
      ),
    );
  }
}
sizeOf + relational patterns: readable breakpoints.

MediaQuery answers "how big is the window?"; LayoutBuilder answers "how much room does my parent give me?". The nuance is decisive for reusable components: a panel shown sometimes full-screen, sometimes in a 320 px column must react to its constraints, not to the window - otherwise it thinks it's on a tablet when it's actually cramped. A useful internal detail: LayoutBuilder's builder runs during the layout phase, when the constraints are known; you therefore can't trigger a synchronous setState inside it, the framework forbids it.

DART
import 'package:flutter/material.dart';

class MasterDetail extends StatelessWidget {
  const MasterDetail({
    super.key,
    required this.list,
    required this.detail,
  });

  final Widget list;
  final Widget detail;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth < 720) {
          return list;
        }
        return Row(
          children: [
            SizedBox(width: 320, child: list),
            const VerticalDivider(width: 1),
            Expanded(child: detail),
          ],
        );
      },
    );
  }
}
The component adapts to the room it receives, wherever it's placed.

For iOS/Android differences, the source of truth on the widget side is Theme.of(context).platform: it derives from defaultTargetPlatform while respecting overrides - essential for testing the iOS rendering from a test or simulating another platform. Many adaptations are already built into the framework: scroll physics (iOS bounce versus Android glow), page transitions, the edge swipe-to-go-back gesture. The .adaptive constructors (Switch.adaptive, CircularProgressIndicator.adaptive) switch to the Cupertino rendering on their own. Reserve your manual branches for genuinely divergent conventions, such as the labeling of actions or the position of dialog buttons.

DART
import 'package:flutter/material.dart';

class SettingsTile extends StatelessWidget {
  const SettingsTile({
    super.key,
    required this.value,
    required this.onChanged,
  });

  final bool value;
  final ValueChanged<bool> onChanged;

  @override
  Widget build(BuildContext context) {
    final platform = Theme.of(context).platform;
    final isApple = platform == TargetPlatform.iOS ||
        platform == TargetPlatform.macOS;
    return ListTile(
      title: Text(
        isApple ? 'Settings' : 'Preferences',
      ),
      trailing: Switch.adaptive(
        value: value,
        onChanged: onChanged,
      ),
    );
  }
}
Switch.adaptive renders a Cupertino toggle on iOS/macOS.

Knowledge check

Make sure you remember the key points of this lesson.

  1. What's the difference between MediaQuery.of(context) and MediaQuery.sizeOf(context)?
    • sizeOf subscribes the widget only to the size aspect: opening the keyboard doesn't rebuild it
    • sizeOf returns the physical size in real screen pixels
    • of is deprecated and will be removed from the framework
  2. When should you prefer LayoutBuilder over MediaQuery to adapt a layout?
    • When the decision depends on the room allocated by the parent, not on the window size
    • When you need to know the device's orientation
    • When you want to avoid a rebuild on a theme change
    • Never: the two are interchangeable
  3. Why avoid Platform.isIOS in widget code?
    • It comes from dart:io, unavailable on the web, and ignores the overrides used by tests and the theme
    • It's noticeably slower than defaultTargetPlatform
    • It returns true on macOS