Construye interfaces que se adaptan a las restricciones del padre, a la ventana y a las convenciones de cada SO.
Abrir esta lección en KodokonMediaQuery describe la ventana lógica, no la pantalla física: tamaño, padding (notch, barra del sistema), viewInsets (teclado), textScaler... Una trampa clásica: MediaQuery.of(context) suscribe el widget a todas estas propiedades. Cuando se abre el teclado, viewInsets cambia y todo widget suscrito se reconstruye, incluidos los que solo leen el ancho. Desde Flutter 3.10, MediaQueryData se expone a través de un InheritedModel: los accesores dirigidos MediaQuery.sizeOf(context), paddingOf, viewInsetsOf... suscriben el widget únicamente al aspecto solicitado. Reflejo de experto: destierra .of(context).size de tu código.
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')),
),
);
}
}MediaQuery responde a "¿de qué tamaño es la ventana?"; LayoutBuilder responde a "¿cuánto espacio me da mi padre?". El matiz es decisivo para los componentes reutilizables: un panel que a veces se muestra a pantalla completa y a veces en una columna de 320 px debe reaccionar a sus restricciones, no a la ventana, si no cree que está en una tablet cuando en realidad está apretado. Un detalle interno útil: el builder de LayoutBuilder se ejecuta durante la fase de layout, cuando ya se conocen las restricciones; por eso no puedes disparar un setState síncrono dentro de él, el framework lo prohíbe.
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),
],
);
},
);
}
}Para las diferencias entre iOS y Android, la fuente de verdad del lado del widget es Theme.of(context).platform: deriva de defaultTargetPlatform respetando los overrides, esencial para probar el renderizado de iOS desde un test o simular otra plataforma. Muchas adaptaciones ya vienen integradas en el framework: la física del scroll (el rebote de iOS frente al glow de Android), las transiciones de página, el gesto de deslizar desde el borde para volver atrás. Los constructores .adaptive (Switch.adaptive, CircularProgressIndicator.adaptive) cambian por sí solos al renderizado Cupertino. Reserva tus ramas manuales para convenciones realmente divergentes, como el etiquetado de las acciones o la posición de los botones de los diálogos.
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,
),
);
}
}