El problema

Una app Flutter que se publica en móviles, tabletas y escritorio necesita algo más que una columna flexible. Una pantalla que se lee bien con 390 píxeles lógicos de ancho pide una tarjeta centrada a 700 y una barra lateral a 1200. Flutter da las medidas, pero no una estructura donde aplicarlas.

Así que las comprobaciones se dispersan. Aquí un MediaQuery.of(context).size.width > 600, allí un LayoutBuilder, y tres widgets más abajo un ternario dentro de un Padding. Cada una parece razonable por separado; juntas forman una política de diseño que no está escrita en ningún sitio.

Los problemas más comunes son:

  • Valores de breakpoint duplicados en decenas de widgets, que van divergiendo a medida que cambia el diseño
  • LayoutBuilder devolviendo las restricciones del padre cuando lo que hacía falta era el dispositivo
  • Diseños que saltan a la variante de tableta en cuanto se gira el móvil a horizontal
  • Árboles de widgets donde la lógica adaptativa es imposible de ver sin leer cada hijo

Cómo lo resuelve flutter_adaptive_layout

AdaptiveLayout recibe un builder por tamaño de pantalla y un child. Mide el dispositivo con MediaQuery, lo traduce a un ScreenSize y construye solo la variante que corresponde, con el child creado una sola vez y entregado al builder elegido.

AdaptiveLayout(
  smallBuilder: (context, child) => child!,
  mediumBuilder: (context, child) => Center(child: child),
  largeBuilder: (context, child) => Row(
    children: [const Sidebar(), Expanded(child: child!)],
  ),
  child: const MyHomePage(),
)

La decisión adaptativa vive ahora en un único widget, en la raíz del subárbol que gobierna, escrita como tres disposiciones en lugar de una cadena de condiciones.

Qué ofrece

  • Disposiciones por tamaño de pantallasmallBuilder, mediumBuilder y largeBuilder, todos opcionales; si falta uno se dibuja el child, así que puedes describir solo los tamaños que te importan
  • Breakpoints bajo tu control — 400 y 600 píxeles lógicos por defecto, redefinibles por widget o para toda la app
  • El child se construye una vez — al cambiar de disposición el contenido se envuelve, no se reconstruye desde cero
  • Estable ante la rotación — la clasificación lee MediaQuery.of(context).size.shortestSide, así que un móvil sigue siendo pantalla pequeña en horizontal
  • Lógica de clasificación conectable — implementa ScreenSizeQualifier para clasificar por ancho, plataforma, estado de la bisagra o como lo haga tu sistema de diseño
  • Sin dependencias — Flutter puro sobre MediaQuery, funciona en iOS, Android, web, macOS, Windows y Linux

Cómo se decide el tamaño de pantalla

El BreakpointsQualifier por defecto compara el lado más corto con dos breakpoints, de forma inclusiva: un valor igual al breakpoint pertenece a la categoría menor.

Tamaño de pantallaCondiciónRango por defectoDispositivo típico
ScreenSize.smallshortestSide <= smallBreakpoint0–400 píxeles lógicosMóviles
ScreenSize.mediumshortestSide <= mediumBreakpoint401–600 píxeles lógicosTabletas pequeñas
ScreenSize.largepor encima de mediumBreakpoint601+ píxeles lógicosTabletas grandes, escritorio

Los breakpoints se resuelven en un orden fijo y gana el primer valor definido: los argumentos de BreakpointsQualifier, después el BreakpointsSetting ancestro más cercano y, por último, los 400 y 600 integrados.

Casos de uso reales

Del móvil a la tableta

Una app que empezó en móviles puede conservar todas sus pantallas y añadir un largeBuilder donde una disposición ancha compense. Las pantallas sin builder para ese tamaño dibujan el child tal cual, así que la migración va pantalla a pantalla en vez de toda de golpe.

Una lista que en el móvil abre el detalle como una ruta nueva puede mostrar lista y detalle lado a lado en una tableta. Las dos variantes se declaran juntas en un mismo widget, de modo que la diferencia se revisa en el propio diff.

Sistemas de diseño con sus propios breakpoints

Envuelve MaterialApp en BreakpointsSetting una vez y todos los AdaptiveLayout del árbol tomarán los números del sistema de diseño. Cambiarlos más adelante es editar una línea, no buscar por todo el proyecto.

Ventanas en web y escritorio

En web y escritorio se mide el tamaño de la ventana, así que la disposición responde mientras el usuario la redimensiona: el mismo código que atiende a una tableta atiende a un navegador reducido.

Cómo empezar

Instala el paquete:

flutter pub add flutter_adaptive_layout

Requiere Dart >=2.18.5 <4.0.0 y Flutter 1.17+.

Impórtalo y envuelve el widget cuya disposición deba adaptarse:

import 'package:flutter_adaptive_layout/flutter_adaptive_layout.dart';

AdaptiveLayout(
  largeBuilder: (context, child) => Center(
    child: SizedBox(width: 600, child: child),
  ),
  child: const ArticleView(), // en pantallas pequeñas y medianas, tal cual
)

Para clasificar las pantallas de otra forma, implementa ScreenSizeQualifier y pásalo como qualifier:

class WidthQualifier extends ScreenSizeQualifier {
  @override
  ScreenSize qualify(BuildContext context) {
    final width = MediaQuery.of(context).size.width;
    if (width < 600) return ScreenSize.small;
    if (width < 1024) return ScreenSize.medium;
    return ScreenSize.large;
  }
}

qualify se ejecuta en cada build, así que debe ser barato y libre de efectos secundarios.

Hay una app completa y ejecutable en el directorio example.

LayoutBuilder informa de las restricciones que le pasa el widget padre, que pueden ser mucho menores que la pantalla. AdaptiveLayout clasifica el dispositivo o la ventana mediante MediaQuery, así que un widget en lo hondo del árbol sigue sabiendo que se ejecuta en una tableta. Usa LayoutBuilder para encajar en el espacio disponible; usa AdaptiveLayout para elegir una disposición según el dispositivo.
No. La clasificación usa MediaQuery.of(context).size.shortestSide, que es igual en vertical y en horizontal, así que un móvil sigue siendo ScreenSize.small al girarlo. Si quieres disposiciones sensibles a la orientación, implementa un ScreenSizeQualifier propio que lea size.width.
No. Cualquier builder puede omitirse y para ese tamaño de pantalla se usa el child. Si no hay ni builder correspondiente ni child, se lanza un UnimplementedError.
Pasa BreakpointsQualifier(smallBreakpoint: ..., mediumBreakpoint: ...) a un AdaptiveLayout concreto, o envuelve la app en BreakpointsSetting para cambiarlos en todas partes. Los valores del constructor tienen prioridad sobre BreakpointsSetting, y este sobre los valores por defecto de 400 y 600.
Sí. Solo depende de MediaQuery, así que funciona en todos los destinos de Flutter. En web y escritorio se mide la ventana, de modo que la disposición responde al redimensionarla.