İçeriğe geç / Skip to content / Zum Inhalt

Flutter: Screen-Based Scaling with MediaQuery

Ahmet Balaman

Last updated:

7 min read

FlutterMediaQueryResponsiveLayoutBuilderSafeAreaLayout
Flutter: Screen-Based Scaling with MediaQuery

MediaQuery is the widget that carries information about the window the app runs in (size, safe area, keyboard height, orientation, text scale) into the widget tree. Decisions such as "the card should be 80 percent of the screen", "show a side menu on wide screens" or "push the content up when the keyboard opens" all draw on it. In this post I first cover the right way to read it (targeted accessors such as MediaQuery.sizeOf), then which jobs belong to MediaQuery and which to LayoutBuilder or FractionallySizedBox, and finally the mistakes I see most often.

Basic Usage

There are two ways to read the window size. The old habit is MediaQuery.of(context).size; that subscribes the widget to every change in MediaQuery. When the keyboard opens, viewInsets changes, and your widget that only cares about the width rebuilds too. Accessors that subscribe to a single property solve this:

@override
Widget build(BuildContext context) {
  final size = MediaQuery.sizeOf(context);
  final padding = MediaQuery.paddingOf(context);

  return Container(
    width: size.width * 0.8,
    margin: EdgeInsets.only(top: padding.top),
    color: Colors.blue,
    child: Center(child: Text('${size.width.toInt()} x ${size.height.toInt()}')),
  );
}

sizeOf triggers a rebuild only when the size changes, paddingOf only when the safe area changes. There is one for every property: orientationOf, viewInsetsOf, devicePixelRatioOf, platformBrightnessOf, textScalerOf; the full list sits under the static methods section of the official MediaQuery API docs. In new code there is no reason left to write MediaQuery.of(context).

What Each Property Tells You

On a phone window, size covers everything, padding marks the top and bottom system bars, and viewInsets marks the area taken by the keyboard

Property Meaning Typical use
size Window size in logical pixels Breakpoint decisions, percentage math
padding System areas such as the notch, status bar and home indicator SafeArea uses this
viewInsets System UI that temporarily covers space, such as the keyboard Bottom spacing for the keyboard
orientation landscape when the width exceeds the height A different layout in landscape
textScaler The user's system text size setting Accessibility testing, scaling fixed heights
devicePixelRatio Physical pixels per logical pixel Choosing an image of the right resolution
platformBrightness The system's light/dark preference Usually ThemeMode.system is enough

The exact definition of every field in the table lives in the MediaQueryData class API docs. One thing to keep in mind: size is the window, not the screen. On a phone they are the same, but on desktop, on the web and in tablet split-screen the window is smaller than the screen. Thinking of your decisions as "window" rather than "screen" avoids surprises.

Percentage Sizing and Its Alternative

Writing size.width * 0.8 for a card that should be 80 percent wide works, but when that card is moved into a side panel, the ratio stays at 80 percent of the window, not of the panel. If you want a ratio of the parent, FractionallySizedBox handles it without any math:

Center(
  child: FractionallySizedBox(
    widthFactor: 0.8,
    child: Card(child: Padding(padding: EdgeInsets.all(16), child: Text('Content'))),
  ),
)

As a rule: if you really mean "a percentage of the window", use MediaQuery; if you mean "a percentage of the parent", use FractionallySizedBox. Most of the time the second is what you meant.

MediaQuery or LayoutBuilder?

They answer different questions. MediaQuery.sizeOf says "how big is the window?"; LayoutBuilder says "how much space was this widget given?". When you open a settings page in the right pane of a tablet, the window is wide but the pane is narrow; LayoutBuilder gives the right answer, MediaQuery the wrong one.

The practical split is this: decisions about the app shell (bottom bar or side rail, one column or two) are made by window size, so MediaQuery fits. The layout inside a reusable component (card horizontal or vertical) is made by the space it was given, so LayoutBuilder fits. Detailed examples and a breakpoint layout are in the LayoutBuilder responsive design post.

Text Scaling

When the user enlarges text in the system settings, the Text widget already applies it; you do not multiply fontSize yourself. MediaQuery.textScalerOf(context) is needed in two cases: fixed heights that must grow with the text, and limiting the growth.

final scaler = MediaQuery.textScalerOf(context);
final rowHeight = scaler.scale(48); // 48 logical pixels, grows with the text scale

// Cap the scaling at 1.3 in a specific subtree:
MediaQuery.withClampedTextScaling(
  maxScaleFactor: 1.3,
  child: const PriceTag(),
)

The old textScaleFactor was a plain multiplier and has been deprecated; as the TextScaler class API docs explain, TextScaler also supports non-linear scaling, so call scale() instead of multiplying by a number.

When to Use It and When Not To

MediaQuery is the right tool for these jobs: choosing the navigation structure by window width, custom drawing that must account for the safe area manually, bottom spacing based on keyboard height, reading the text scale for accessibility, and building an entirely different page layout per orientation.

In these cases another tool is better:

  • If the space given to the component itself matters, LayoutBuilder; if only the orientation matters, OrientationBuilder (which also looks at the parent's constraints).
  • For a percentage of the parent, FractionallySizedBox; for ratios between siblings, Expanded. Recipes for both are in the layout control post with Expanded, Flexible and Spacer.
  • Instead of computing safe-area padding by hand, SafeArea. Instead of manual keyboard spacing, the default resizeToAvoidBottomInset: true behavior of Scaffold.
  • Instead of multiplying the font size by the width, Theme.of(context).textTheme and a few breakpoints. Text that grows with the percentage becomes huge on tablets, unreadable on small phones, and overrides the user's accessibility setting.

Common Mistakes

1. Reading MediaQuery in initState

Symptom: dependOnInheritedWidgetOfExactType<MediaQuery>() or dependOnInheritedElement() was called before _MyPageState.initState() completed. While initState runs, the widget is not yet attached to the tree. Read the value in build; if it must be computed once, didChangeDependencies is the right place.

2. Multiplying fontSize by textScaleFactor

Symptom: When the user sets system text to 150 percent, the text doubles and the layout falls apart. Text already applies the scale; writing fontSize: 16 * factor means scaling twice. Remove the multiplication; if you want to scale fixed heights, use textScalerOf(context).scale(...).

3. Manual keyboard padding inside a Scaffold

Symptom: When the keyboard opens, twice the keyboard height of empty space appears below the form. Scaffold already shrinks the body for the keyboard; adding padding: EdgeInsets.only(bottom: viewInsets.bottom) on top doubles the gap. Either leave it to Scaffold, or set resizeToAvoidBottomInset: false and manage the spacing yourself; not both.

4. Checking the window width inside a side panel widget

Symptom: On a tablet, a card opened in the right pane picks the "wide" layout because the window is wide, and overflows. The space given to a widget is independent of the window. Make the decision from constraints.maxWidth of a LayoutBuilder.

Mini Scenario: Navigation by Width

Picture a notes app. On a phone there is a NavigationBar at the bottom; on tablet and desktop a NavigationRail on the left; in both cases the content stays inside the safe area:

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

  @override
  State<HomeShell> createState() => _HomeShellState();
}

class _HomeShellState extends State<HomeShell> {
  int _index = 0;

  static const _destinations = [
    (icon: Icons.notes, label: 'Notes'),
    (icon: Icons.search, label: 'Search'),
    (icon: Icons.settings, label: 'Settings'),
  ];

  @override
  Widget build(BuildContext context) {
    final isWide = MediaQuery.sizeOf(context).width >= 600;
    final body = SafeArea(child: NotesPage(index: _index));

    if (isWide) {
      return Scaffold(
        body: Row(
          children: [
            NavigationRail(
              selectedIndex: _index,
              onDestinationSelected: (i) => setState(() => _index = i),
              labelType: NavigationRailLabelType.all,
              destinations: [
                for (final d in _destinations)
                  NavigationRailDestination(icon: Icon(d.icon), label: Text(d.label)),
              ],
            ),
            const VerticalDivider(width: 1),
            Expanded(child: body),
          ],
        ),
      );
    }

    return Scaffold(
      body: body,
      bottomNavigationBar: NavigationBar(
        selectedIndex: _index,
        onDestinationSelected: (i) => setState(() => _index = i),
        destinations: [
          for (final d in _destinations)
            NavigationDestination(icon: Icon(d.icon), label: d.label),
        ],
      ),
    );
  }
}

Because the decision depends on the window width, MediaQuery.sizeOf is the right tool; this is a shell decision, not a component decision. Since sizeOf is used, this widget does not rebuild when the keyboard opens; it only runs when the window rotates or is resized on desktop. SafeArea takes care of the safe area, no manual math with paddingOf. The layout inside NotesPage (whether the notes are in one column or two) depends not on the window but on the space it was given; that is where LayoutBuilder belongs. Choosing between the two navigation widgets is covered in the NavigationBar versus BottomNavigationBar comparison.

Frequently Asked Questions

What is the difference between MediaQuery.of and MediaQuery.sizeOf?

MediaQuery.of(context) subscribes the widget to every change in MediaQuery; it rebuilds even when the keyboard opens. MediaQuery.sizeOf(context) triggers a rebuild only when the size changes. paddingOf, viewInsetsOf and textScalerOf follow the same logic.

Should I use MediaQuery or LayoutBuilder?

If you are making a shell decision by window size (bottom bar or side rail), MediaQuery. If you are laying out a component by the space it was given, LayoutBuilder. In reusable components the second is almost always right.

Should I scale font size by screen width?

No. Text already applies the user's system text setting; multiplying by the width produces giant text on tablets and breaks the accessibility setting. Switching textTheme styles at a few breakpoints is enough.

Why does the content overflow when the keyboard opens?

Scaffold shrinks the body for the keyboard by default; if there is an overflow, the body is not scrollable. Put the form in a SingleChildScrollView or ListView. Manual spacing with viewInsets is only needed when you set resizeToAvoidBottomInset: false.

Comments