Kodokon kodokon.com

Adaptiv: LayoutBuilder, MediaQuery und Plattformen

Baue Oberflächen, die sich an die Constraints des Elternteils, an das Fenster und an die Konventionen jedes Betriebssystems anpassen.

9 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

MediaQuery beschreibt das logische Fenster, nicht den physischen Bildschirm: Größe, padding (Notch, Systemleiste), viewInsets (Tastatur), textScaler... Eine klassische Falle: MediaQuery.of(context) meldet das Widget bei allen diesen Eigenschaften an. Wenn sich die Tastatur öffnet, ändert sich viewInsets und jedes angemeldete Widget wird neu gebaut - auch die, die nur die Breite lesen. Seit Flutter 3.10 wird MediaQueryData über ein InheritedModel bereitgestellt: Die gezielten Zugriffsmethoden MediaQuery.sizeOf(context), paddingOf, viewInsetsOf... melden das Widget nur bei dem angefragten Aspekt an. Reflex der Profis: Verbanne .of(context).size aus deiner Codebasis.

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 + relationale Patterns: lesbare Breakpoints.

MediaQuery beantwortet die Frage "Wie groß ist das Fenster?"; LayoutBuilder beantwortet "Wie viel Platz gibt mir mein Elternteil?". Dieser Unterschied ist für wiederverwendbare Komponenten entscheidend: Ein Panel, das mal bildschirmfüllend, mal in einer 320 px breiten Spalte angezeigt wird, muss auf seine Constraints reagieren, nicht auf das Fenster - sonst hält es sich für ein Tablet, während es in Wahrheit eingezwängt ist. Ein nützliches inneres Detail: Der builder von LayoutBuilder läuft während der Layout-Phase, wenn die Constraints bekannt sind; du kannst darin daher kein synchrones setState auslösen, das Framework verbietet es.

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),
          ],
        );
      },
    );
  }
}
Die Komponente passt sich dem Platz an, den sie bekommt, wo auch immer sie steht.

Für die Unterschiede zwischen iOS und Android ist auf Widget-Seite Theme.of(context).platform die Quelle der Wahrheit: Sie leitet sich von defaultTargetPlatform ab und respektiert dabei die Overrides - unverzichtbar, um die iOS-Darstellung aus einem Test heraus zu prüfen oder eine andere Plattform zu simulieren. Viele Anpassungen sind bereits im Framework eingebaut: die Scroll-Physik (das Nachfedern unter iOS gegenüber dem Leuchten unter Android), die Seitenübergänge, die Wischgeste vom Rand zum Zurückgehen. Die .adaptive-Konstruktoren (Switch.adaptive, CircularProgressIndicator.adaptive) wechseln von selbst zur Cupertino-Darstellung. Hebe dir eigene Verzweigungen für wirklich abweichende Konventionen auf, etwa die Beschriftung von Aktionen oder die Position der Buttons in Dialogen.

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 stellt unter iOS/macOS einen Cupertino-Schalter dar.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Was ist der Unterschied zwischen MediaQuery.of(context) und MediaQuery.sizeOf(context)?
    • sizeOf meldet das Widget nur beim Aspekt Größe an: Das Öffnen der Tastatur baut es nicht neu
    • sizeOf liefert die physische Größe in echten Bildschirmpixeln
    • of ist veraltet und wird aus dem Framework entfernt
  2. Wann solltest du LayoutBuilder gegenüber MediaQuery bevorzugen, um ein Layout anzupassen?
    • Wenn die Entscheidung von dem Platz abhängt, den das Elternteil zuteilt, und nicht von der Fenstergröße
    • Wenn du die Ausrichtung des Geräts wissen musst
    • Wenn du einen Rebuild bei einem Wechsel des Themes vermeiden willst
    • Nie: Die beiden sind austauschbar
  3. Warum solltest du Platform.isIOS im Widget-Code vermeiden?
    • Es stammt aus dart:io, das im Web nicht verfügbar ist, und ignoriert die Overrides, die von den Tests und vom Theme genutzt werden
    • Es ist spürbar langsamer als defaultTargetPlatform
    • Es liefert unter macOS true