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

Flutter: Bildschirmabhängige Skalierung mit MediaQuery

Ahmet Balaman

Zuletzt aktualisiert:

6 Min. Lesezeit

FlutterMediaQueryResponsiveLayoutBuilderSafeAreaLayout
Flutter: Bildschirmabhängige Skalierung mit MediaQuery

MediaQuery ist das Widget, das Informationen über das Fenster, in dem die App läuft (Größe, sicherer Bereich, Tastaturhöhe, Ausrichtung, Textskalierung), in den Widget-Baum trägt. Entscheidungen wie „die Karte soll 80 Prozent des Bildschirms einnehmen“, „auf breiten Bildschirmen ein Seitenmenü zeigen“ oder „bei geöffneter Tastatur den Inhalt nach oben schieben“ speisen sich alle daraus. In diesem Beitrag zeige ich zuerst die richtige Leseweise (gezielte Zugriffe wie MediaQuery.sizeOf), dann welche Aufgabe zu MediaQuery und welche zu LayoutBuilder oder FractionallySizedBox gehört, und zum Schluss die häufigsten Fehler.

Grundlegende Verwendung

Es gibt zwei Wege, die Fenstergröße zu lesen. Die alte Gewohnheit ist MediaQuery.of(context).size; das bindet das Widget an jede Änderung in MediaQuery. Öffnet sich die Tastatur, ändert sich viewInsets, und Ihr Widget, das sich nur für die Breite interessiert, wird ebenfalls neu gebaut. Zugriffe, die sich an eine einzelne Eigenschaft binden, lösen das:

@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 löst einen Rebuild nur bei geänderter Größe aus, paddingOf nur bei geändertem sicheren Bereich. Für jede Eigenschaft gibt es ein Gegenstück: orientationOf, viewInsetsOf, devicePixelRatioOf, platformBrightnessOf, textScalerOf; die vollständige Liste steht im Abschnitt der statischen Methoden der offiziellen MediaQuery-API-Dokumentation. In neuem Code gibt es keinen Grund mehr, MediaQuery.of(context) zu schreiben.

Welche Eigenschaft sagt was?

In einem Telefonfenster umfasst size alles, padding markiert die oberen und unteren Systemleisten und viewInsets den Bereich der Tastatur

Eigenschaft Bedeutung Typische Verwendung
size Fenstergröße in logischen Pixeln Breakpoint-Entscheidungen, Prozentrechnung
padding Systembereiche wie Notch, Statusleiste und Home-Indikator SafeArea nutzt genau das
viewInsets System-UI, die vorübergehend Platz belegt, etwa die Tastatur Unterer Abstand bei Tastatur
orientation landscape, wenn die Breite größer als die Höhe ist Anderes Layout im Querformat
textScaler Die Textgrößeneinstellung des Nutzers im System Barrierefreiheitstests, feste Höhen skalieren
devicePixelRatio Physische Pixel pro logischem Pixel Bild in passender Auflösung wählen
platformBrightness Hell/Dunkel-Präferenz des Systems Meist reicht ThemeMode.system

Die genaue Bedeutung jedes Felds aus der Tabelle steht in der API-Dokumentation der Klasse MediaQueryData. Ein Punkt, den Sie im Kopf behalten sollten: size ist das Fenster, nicht der Bildschirm. Auf dem Telefon sind beide identisch, aber auf dem Desktop, im Web und im Split-Screen des Tablets ist das Fenster kleiner als der Bildschirm. Wer seine Entscheidungen als „Fenster“ statt „Bildschirm“ denkt, erlebt weniger Überraschungen.

Prozentuale Größen und die Alternative

size.width * 0.8 für eine Karte mit 80 Prozent Breite funktioniert, aber wird diese Karte in ein Seitenpanel verschoben, bleibt das Verhältnis bei 80 Prozent des Fensters statt des Panels. Wollen Sie ein Verhältnis zum Eltern-Widget, erledigt FractionallySizedBox das ohne Rechnerei:

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

Als Regel: Ist wirklich „Prozent des Fensters“ gemeint, MediaQuery; ist „Prozent des Eltern-Widgets“ gemeint, FractionallySizedBox. Meistens ist Letzteres gemeint.

MediaQuery oder LayoutBuilder?

Beide beantworten unterschiedliche Fragen. MediaQuery.sizeOf sagt: „Wie groß ist das Fenster?“ LayoutBuilder sagt: „Wie viel Platz hat dieses Widget bekommen?“ Öffnen Sie eine Einstellungsseite im rechten Panel eines Tablets, ist das Fenster breit, das Panel aber schmal; LayoutBuilder liefert die richtige Antwort, MediaQuery die falsche.

Die praktische Trennung: Entscheidungen zum App-Gerüst (untere Leiste oder seitliche Rail, eine Spalte oder zwei) fallen nach Fenstergröße, dafür passt MediaQuery. Das Layout innerhalb einer wiederverwendbaren Komponente (Karte horizontal oder vertikal) richtet sich nach dem zugewiesenen Platz, dafür passt LayoutBuilder. Ausführliche Beispiele und ein Breakpoint-Layout stehen im Beitrag zu responsivem Design mit LayoutBuilder.

Textskalierung

Vergrößert der Nutzer den Text in den Systemeinstellungen, wendet das Text-Widget das bereits an; Sie multiplizieren fontSize nicht selbst. MediaQuery.textScalerOf(context) brauchen Sie in zwei Fällen: feste Höhen, die mit dem Text wachsen müssen, und das Begrenzen des Wachstums.

final scaler = MediaQuery.textScalerOf(context);
final rowHeight = scaler.scale(48); // 48 logische Pixel, wächst mit der Textskalierung

// Skalierung in einem bestimmten Teilbaum auf 1.3 begrenzen:
MediaQuery.withClampedTextScaling(
  maxScaleFactor: 1.3,
  child: const PriceTag(),
)

Das alte textScaleFactor war ein einfacher Multiplikator und ist veraltet; wie die API-Dokumentation der Klasse TextScaler erklärt, unterstützt TextScaler auch nichtlineare Skalierung, deshalb rufen Sie scale() auf, statt mit einer Zahl zu multiplizieren.

Wann verwenden – und wann nicht?

MediaQuery ist das richtige Werkzeug für diese Aufgaben: die Navigationsstruktur nach Fensterbreite wählen, eigene Zeichnungen, die den sicheren Bereich manuell berücksichtigen müssen, unterer Abstand nach Tastaturhöhe, die Textskalierung für Barrierefreiheit lesen und ein komplett anderes Seitenlayout je Ausrichtung.

In diesen Fällen ist ein anderes Werkzeug besser:

  • Zählt der Platz, den die Komponente selbst bekommt, LayoutBuilder; zählt nur die Ausrichtung, OrientationBuilder (der ebenfalls auf die Constraints des Eltern-Widgets schaut).
  • Für einen Prozentsatz des Eltern-Widgets FractionallySizedBox; für Verhältnisse zwischen Geschwistern Expanded. Rezepte für beide stehen im Beitrag zur Layout-Steuerung mit Expanded, Flexible und Spacer.
  • Statt den Abstand zum sicheren Bereich von Hand zu berechnen, SafeArea. Statt manuellem Tastaturabstand das Standardverhalten resizeToAvoidBottomInset: true von Scaffold.
  • Statt die Schriftgröße mit der Breite zu multiplizieren, Theme.of(context).textTheme und ein paar Breakpoints. Text, der prozentual wächst, wird auf Tablets riesig, auf kleinen Telefonen unlesbar und überschreibt die Barrierefreiheitseinstellung des Nutzers.

Häufige Fehler

1. MediaQuery in initState lesen

Symptom: dependOnInheritedWidgetOfExactType<MediaQuery>() or dependOnInheritedElement() was called before _MyPageState.initState() completed. Während initState läuft, hängt das Widget noch nicht im Baum. Lesen Sie den Wert in build; soll er einmalig berechnet werden, ist didChangeDependencies der richtige Ort.

2. fontSize mit textScaleFactor multiplizieren

Symptom: Stellt der Nutzer den Systemtext auf 150 Prozent, verdoppelt sich der Text und das Layout zerfällt. Text wendet die Skalierung bereits an; fontSize: 16 * factor bedeutet doppelte Skalierung. Entfernen Sie die Multiplikation; wollen Sie feste Höhen skalieren, nutzen Sie textScalerOf(context).scale(...).

3. Manuelles Tastatur-Padding im Scaffold

Symptom: Öffnet sich die Tastatur, bleibt unter dem Formular ein Leerraum von doppelter Tastaturhöhe. Scaffold verkleinert den Body bereits für die Tastatur; ein zusätzliches padding: EdgeInsets.only(bottom: viewInsets.bottom) verdoppelt die Lücke. Überlassen Sie es entweder dem Scaffold, oder setzen Sie resizeToAvoidBottomInset: false und verwalten den Abstand selbst; nicht beides.

4. Im Widget eines Seitenpanels auf die Fensterbreite schauen

Symptom: Auf dem Tablet wählt eine im rechten Panel geöffnete Karte das „breite“ Layout, weil das Fenster breit ist, und läuft über. Der Platz eines Widgets ist vom Fenster unabhängig. Treffen Sie die Entscheidung anhand von constraints.maxWidth eines LayoutBuilder.

Mini-Szenario: Navigation nach Breite

Stellen Sie sich eine Notiz-App vor. Auf dem Telefon gibt es unten eine NavigationBar, auf Tablet und Desktop links eine NavigationRail; der Inhalt bleibt in beiden Fällen im sicheren Bereich:

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: 'Notizen'),
    (icon: Icons.search, label: 'Suchen'),
    (icon: Icons.settings, label: 'Einstellungen'),
  ];

  @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),
        ],
      ),
    );
  }
}

Weil die Entscheidung von der Fensterbreite abhängt, ist MediaQuery.sizeOf das richtige Werkzeug; das ist eine Gerüst-Entscheidung, keine Komponenten-Entscheidung. Da sizeOf verwendet wird, baut sich dieses Widget beim Öffnen der Tastatur nicht neu; es läuft nur, wenn das Fenster gedreht oder auf dem Desktop in der Größe geändert wird. Den sicheren Bereich übernimmt SafeArea, keine Handrechnung mit paddingOf. Das Layout innerhalb von NotesPage (ob die Notizen in einer oder zwei Spalten stehen) hängt nicht vom Fenster ab, sondern vom zugewiesenen Platz; dort gehört LayoutBuilder hin. Die Wahl zwischen den beiden Navigations-Widgets behandelt der Vergleich von NavigationBar und BottomNavigationBar.

Häufig gestellte Fragen

Was ist der Unterschied zwischen MediaQuery.of und MediaQuery.sizeOf?

MediaQuery.of(context) bindet das Widget an jede Änderung in MediaQuery; es wird selbst beim Öffnen der Tastatur neu gebaut. MediaQuery.sizeOf(context) löst einen Rebuild nur bei geänderter Größe aus. paddingOf, viewInsetsOf und textScalerOf folgen derselben Logik.

Sollte ich MediaQuery oder LayoutBuilder verwenden?

Treffen Sie eine Gerüst-Entscheidung nach Fenstergröße (untere Leiste oder seitliche Rail), MediaQuery. Bauen Sie das Layout einer Komponente nach dem zugewiesenen Platz, LayoutBuilder. In wiederverwendbaren Komponenten ist fast immer Letzteres richtig.

Sollte ich die Schriftgröße nach Bildschirmbreite skalieren?

Nein. Text wendet die Systemeinstellung des Nutzers bereits an; die Multiplikation mit der Breite erzeugt riesigen Text auf Tablets und bricht die Barrierefreiheitseinstellung. Es reicht, textTheme-Stile an wenigen Breakpoints zu wechseln.

Warum läuft der Inhalt über, wenn sich die Tastatur öffnet?

Scaffold verkleinert den Body standardmäßig für die Tastatur; gibt es einen Overflow, ist der Body nicht scrollbar. Setzen Sie das Formular in eine SingleChildScrollView oder ListView. Manueller Abstand über viewInsets ist nur nötig, wenn Sie resizeToAvoidBottomInset: false setzen.

Kommentare