Kodokon kodokon.com

Adaptatif : LayoutBuilder, MediaQuery et plateformes

Construisez des interfaces qui s'adaptent aux contraintes du parent, à la fenêtre et aux conventions de chaque OS.

9 min · 3 questions

Ouvrir cette leçon dans Kodokon

MediaQuery décrit la fenêtre logique, pas l'écran physique : taille, padding (encoche, barre système), viewInsets (clavier), textScaler... Piège classique : MediaQuery.of(context) abonne le widget à toutes ces propriétés. Quand le clavier s'ouvre, viewInsets change et tous les widgets abonnés se reconstruisent - y compris ceux qui ne lisaient que la largeur. Depuis Flutter 3.10, MediaQueryData est exposé via un InheritedModel : les accesseurs ciblés MediaQuery.sizeOf(context), paddingOf, viewInsetsOf... n'abonnent le widget qu'à l'aspect demandé. Réflexe d'expert : bannissez .of(context).size de votre base de code.

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('Tuile $i')),
      ),
    );
  }
}
sizeOf + patterns relationnels : des breakpoints lisibles.

MediaQuery répond à « quelle est la taille de la fenêtre ? » ; LayoutBuilder répond à « combien de place mon parent me donne-t-il ? ». La nuance est décisive pour les composants réutilisables : un panneau affiché tantôt plein écran, tantôt dans une colonne de 320 px doit réagir à ses contraintes, pas à la fenêtre - sinon il se croira sur tablette alors qu'il est à l'étroit. Détail interne utile : le builder de LayoutBuilder s'exécute pendant la phase de layout, quand les contraintes sont connues ; vous ne pouvez donc pas y déclencher un setState synchrone, le framework l'interdit.

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),
          ],
        );
      },
    );
  }
}
Le composant s'adapte à la place reçue, où qu'il soit posé.

Pour les différences iOS/Android, la source de vérité côté widgets est Theme.of(context).platform : elle dérive de defaultTargetPlatform tout en respectant les overrides - indispensable pour tester le rendu iOS depuis un test ou simuler une autre plateforme. Beaucoup d'adaptations sont déjà intégrées au framework : physique de défilement (rebond iOS contre lueur Android), transitions de pages, geste de retour depuis le bord. Les constructeurs .adaptive (Switch.adaptive, CircularProgressIndicator.adaptive) basculent d'eux-mêmes sur le rendu Cupertino. Réservez vos branches manuelles aux conventions réellement divergentes, comme le libellé des actions ou la position des boutons de dialogue.

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 ? 'Réglages' : 'Paramètres',
      ),
      trailing: Switch.adaptive(
        value: value,
        onChanged: onChanged,
      ),
    );
  }
}
Switch.adaptive rend un interrupteur Cupertino sur iOS/macOS.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Quelle différence entre MediaQuery.of(context) et MediaQuery.sizeOf(context) ?
    • sizeOf n'abonne le widget qu'à l'aspect size : l'ouverture du clavier ne le reconstruit pas
    • sizeOf renvoie la taille physique en pixels réels de l'écran
    • of est déprécié et sera retiré du framework
  2. Quand préférer LayoutBuilder à MediaQuery pour adapter une mise en page ?
    • Quand la décision dépend de la place allouée par le parent, pas de la taille de la fenêtre
    • Quand on a besoin de connaître l'orientation de l'appareil
    • Quand on veut éviter un rebuild au changement de thème
    • Jamais : les deux sont interchangeables
  3. Pourquoi éviter Platform.isIOS dans le code des widgets ?
    • Il vient de dart:io, indisponible sur le web, et ignore les overrides utilisés par les tests et le thème
    • Il est nettement plus lent que defaultTargetPlatform
    • Il renvoie true sur macOS