Construisez des interfaces qui s'adaptent aux contraintes du parent, à la fenêtre et aux conventions de chaque OS.
Ouvrir cette leçon dans KodokonMediaQuery 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.
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')),
),
);
}
}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.
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),
],
);
},
);
}
}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.
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,
),
);
}
}