Kodokon kodokon.com

Adaptativo: LayoutBuilder, MediaQuery y plataformas

Construye interfaces que se adaptan a las restricciones del padre, a la ventana y a las convenciones de cada SO.

9 min · 3 preguntas

Abrir esta lección en Kodokon

MediaQuery 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.

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 + patrones relacionales: breakpoints legibles.

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.

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),
          ],
        );
      },
    );
  }
}
El componente se adapta al espacio que recibe, esté donde esté colocado.

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.

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 muestra un toggle Cupertino en iOS/macOS.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Cuál es la diferencia entre MediaQuery.of(context) y MediaQuery.sizeOf(context)?
    • sizeOf suscribe el widget únicamente al aspecto del tamaño: abrir el teclado no lo reconstruye
    • sizeOf devuelve el tamaño físico en píxeles reales de la pantalla
    • of está obsoleto y se eliminará del framework
  2. ¿Cuándo conviene preferir LayoutBuilder frente a MediaQuery para adaptar un layout?
    • Cuando la decisión depende del espacio que asigna el padre, no del tamaño de la ventana
    • Cuando necesitas conocer la orientación del dispositivo
    • Cuando quieres evitar un rebuild al cambiar el tema
    • Nunca: los dos son intercambiables
  3. ¿Por qué evitar Platform.isIOS en el código de los widgets?
    • Proviene de dart:io, no disponible en la web, e ignora los overrides que usan los tests y el tema
    • Es notablemente más lento que defaultTargetPlatform
    • Devuelve true en macOS