Saltar al contenido principal

Migrar a Material 3

Aprende cómo migrar la UI de tu aplicación de Flutter de Material 2 a Material 3.

Resumen

#

La biblioteca Material se ha actualizado para coincidir con la especificación de diseño de Material 3. Los cambios incluyen nuevos componentes y temas de componentes, visuales de componentes actualizados y mucho más. Muchas de estas actualizaciones son transparentes. Verás la nueva versión de un widget afectado al volver a compilar tu aplicación con la versión 3.16 (o posterior). Pero también se requiere algo de trabajo manual para completar la migración.

Guía de migración

#

Antes del lanzamiento de la versión 3.16, podías optar por los cambios de Material 3 estableciendo la bandera useMaterial3 como true. A partir del lanzamiento de Flutter 3.16 (noviembre de 2023), useMaterial3 es true por defecto.

Por cierto, puedes revertir al comportamiento de Material 2 en tu aplicación estableciendo useMaterial3 en false. Sin embargo, esto es solo una solución temporal. La bandera useMaterial3 y la implementación de Material 2 eventualmente se eliminarán como parte de la política de obsolescencia de Flutter.

Colores

#

Los valores predeterminados para ThemeData.colorScheme están actualizados para coincidir con la especificación de diseño de Material 3.

El constructor ColorScheme.fromSeed genera un ColorScheme derivado del seedColor proporcionado. Los colores generados por este constructor están diseñados para funcionar bien juntos y cumplir con los requisitos de contraste para la accesibilidad en el sistema de diseño de Material 3.

Al actualizar a la versión 3.16, tu interfaz de usuario podría verse un poco extraña sin el ColorScheme correcto. Para solucionar esto, migra al ColorScheme generado a partir del constructor ColorScheme.fromSeed.

Código antes de la migración:

dart
theme: ThemeData(
  colorScheme: ColorScheme.light(primary: Colors.blue),
),

Código después de la migración:

dart
theme: ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),

Para generar un esquema de colores dinámico basado en contenido, utiliza el método estático ColorScheme.fromImageProvider. Para ver un ejemplo de generación de un esquema de colores, consulta la muestra de ColorScheme a partir de una imagen de red.

Los cambios en Flutter Material 3 incluyen un nuevo color de fondo. ColorScheme.surfaceTint indica un widget elevado. Algunos widgets utilizan colores diferentes.

Para devolver la UI de tu aplicación a su comportamiento anterior (lo cual no recomendamos):

  • Establece Colors.grey[50]! en ColorScheme.background (cuando el tema es Brightness.light).
  • Establece Colors.grey[850]! en ColorScheme.background (cuando el tema es Brightness.dark).

Código antes de la migración:

dart
theme: ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),

Código después de la migración:

dart
theme: ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith(
    background: Colors.grey[50]!,
  ),
),
dart
darkTheme: ThemeData(
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.deepPurple,
    brightness: Brightness.dark,
  ).copyWith(background: Colors.grey[850]!),
),

El valor de ColorScheme.surfaceTint indica la elevación de un componente en Material 3. Algunos widgets pueden usar tanto surfaceTint como shadowColor para indicar elevación (por ejemplo, Card y ElevatedButton) y otros pueden usar solo surfaceTint para indicar la elevación (como AppBar).

Para volver al comportamiento anterior del widget, establece Colors.transparent en ColorScheme.surfaceTint en el tema. Para diferenciar la sombra de un widget del contenido (cuando no tiene sombra), establece el color ColorScheme.shadow en la propiedad shadowColor en el tema del widget sin un color de sombra predeterminado.

Código antes de la migración:

dart
theme: ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),

Código después de la migración:

dart
theme: ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple).copyWith(
    surfaceTint: Colors.transparent,
  ),
  appBarTheme: AppBarTheme(
   elevation: 4.0,
   shadowColor: Theme.of(context).colorScheme.shadow,
 ),
),

El ElevatedButton ahora se estiliza con una nueva combinación de colores. Anteriormente, cuando el flag useMaterial3 estaba configurado en false, ElevatedButton se estilizaba con ColorScheme.primary para el fondo y ColorScheme.onPrimary para el primer plano. Para lograr los mismos visuales, cambia al nuevo Widget FilledButton sin los cambios de elevación o sombra paralela.

Código antes de la migración:

dart
ElevatedButton(
  onPressed: () {},
  child: const Text('Button'),
),

Código después de la migración:

dart
ElevatedButton(
  style: ElevatedButton.styleFrom(
    backgroundColor: Theme.of(context).colorScheme.primary,
    foregroundColor: Theme.of(context).colorScheme.onPrimary,
  ),
  onPressed: () {},
  child: const Text('Button'),
),

Tipografía

#

Los valores predeterminados para ThemeData.textTheme se han actualizado para coincidir con los valores predeterminados de Material 3. Los cambios incluyen el tamaño de fuente, grosor de fuente, espaciado entre letras y altura de línea actualizados. Para más detalles, consulta la documentación de TextTheme.

Como se muestra en el siguiente ejemplo, antes de la versión 3.16, un widget Text con una cadena larga utilizando TextTheme.bodyLarge en un diseño restringido ajustaba el texto en dos líneas. Sin embargo, la versión 3.16 ajusta el texto en tres líneas. Si necesitas obtener el comportamiento anterior, ajusta el estilo de texto y, si es necesario, el espaciado de las letras.

Código antes de la migración:

dart
ConstrainedBox(
  constraints: const BoxConstraints(maxWidth: 200),
    child: Text(
      'This is a very long text that should wrap to multiple lines.',
      style: Theme.of(context).textTheme.bodyLarge,
  ),
),

Código después de la migración:

dart
ConstrainedBox(
  constraints: const BoxConstraints(maxWidth: 200),
    child: Text(
      'This is a very long text that should wrap to multiple lines.',
      style: Theme.of(context).textTheme.bodyMedium!.copyWith(
        letterSpacing: 0.0,
      ),
  ),
),

Componentes

#

Algunos componentes no pudieron simplemente actualizarse para coincidir con la especificación de diseño de Material 3, sino que necesitaron una implementación completamente nueva. Estos componentes requieren una migración manual, ya que el SDK de Flutter no sabe qué quieres exactamente.

Reemplaza el widget BottomNavigationBar del estilo de Material 2 con el nuevo widget NavigationBar. Es ligeramente más alto, contiene indicadores de navegación en forma de píldora y utiliza nuevos mapeos de colores.

Código antes de la migración:

dart
BottomNavigationBar(
  items: const <BottomNavigationBarItem>[
    BottomNavigationBarItem(
      icon: Icon(Icons.home),
      label: 'Home',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.business),
      label: 'Business',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.school),
      label: 'School',
    ),
  ],
),

Código después de la migración:

dart
NavigationBar(
  destinations: const <Widget>[
    NavigationDestination(
      icon: Icon(Icons.home),
      label: 'Home',
    ),
    NavigationDestination(
      icon: Icon(Icons.business),
      label: 'Business',
    ),
    NavigationDestination(
      icon: Icon(Icons.school),
      label: 'School',
    ),
  ],
),

Consulta la muestra completa sobre la migración de BottomNavigationBar a NavigationBar.

Reemplaza el widget Drawer con NavigationDrawer, el cual proporciona indicadores de navegación en forma de píldora, esquinas redondeadas y nuevos mapeos de colores.

Código antes de la migración:

dart
Drawer(
  child: ListView(
    children: <Widget>[
      DrawerHeader(
        child: Text(
          'Drawer Header',
          style: Theme.of(context).textTheme.titleLarge,
        ),
      ),
      ListTile(
        leading: const Icon(Icons.message),
        title: const Text('Messages'),
        onTap: () { },
      ),
      ListTile(
        leading: const Icon(Icons.account_circle),
        title: const Text('Profile'),
        onTap: () {},
      ),
      ListTile(
        leading: const Icon(Icons.settings),
        title: const Text('Settings'),
        onTap: () { },
      ),
    ],
  ),
),

Código después de la migración:

dart
NavigationDrawer(
  children: <Widget>[
    DrawerHeader(
      child: Text(
        'Drawer Header',
        style: Theme.of(context).textTheme.titleLarge,
      ),
    ),
    const NavigationDrawerDestination(
      icon: Icon(Icons.message),
      label: Text('Messages'),
    ),
    const NavigationDrawerDestination(
      icon: Icon(Icons.account_circle),
      label: Text('Profile'),
    ),
    const NavigationDrawerDestination(
      icon: Icon(Icons.settings),
      label: Text('Settings'),
    ),
  ],
),

Consulta la muestra completa sobre la migración de Drawer a NavigationDrawer.

Material 3 introduce barras de aplicaciones medianas y grandes que muestran un título más grande antes de hacer scroll. En lugar de una sombra paralela, se utiliza el color ColorScheme.surfaceTint para crear una separación del contenido al hacer scroll.

El siguiente código demuestra cómo implementar la barra de aplicaciones mediana:

dart
CustomScrollView(
  slivers: <Widget>[
    const SliverAppBar.medium(
      title: Text('Title'),
    ),
    SliverToBoxAdapter(
      child: Card(
        child: SizedBox(
          height: 1200,
          child: Padding(
            padding: const EdgeInsets.fromLTRB(8, 100, 8, 100),
            child: Text(
              'Here be scrolling content...',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
          ),
        ),
      ),
    ),
  ],
),

Ahora hay dos tipos de widgets TabBar: primarios y secundarios. Las pestañas secundarias se utilizan dentro de un área de contenido para separar aún más el contenido relacionado y establecer una jerarquía. Consulta el ejemplo de TabBar.secondary.

La nueva propiedad TabBar.tabAlignment especifica la alineación horizontal de las pestañas.

La siguiente muestra enseña cómo modificar la alineación de pestañas en una TabBar con scroll:

dart
AppBar(
  title: const Text('Title'),
  bottom: const TabBar(
    tabAlignment: TabAlignment.start,
    isScrollable: true,
    tabs: <Widget>[
      Tab(
        icon: Icon(Icons.cloud_outlined),
      ),
      Tab(
        icon: Icon(Icons.beach_access_sharp),
      ),
      Tab(
        icon: Icon(Icons.brightness_5_sharp),
      ),
    ],
  ),
),

SegmentedButton, una versión actualizada de ToggleButtons, utiliza esquinas completamente redondeadas, difiere en altura y tamaño de diseño, y utiliza un Set de Dart para determinar los elementos seleccionados.

Código antes de la migración:

dart
enum Weather { cloudy, rainy, sunny }

ToggleButtons(
  isSelected: const [false, true, false],
  onPressed: (int newSelection) { },
  children: const <Widget>[
    Icon(Icons.cloud_outlined),
    Icon(Icons.beach_access_sharp),
    Icon(Icons.brightness_5_sharp),
  ],
),

Código después de la migración:

dart
enum Weather { cloudy, rainy, sunny }

SegmentedButton<Weather>(
  selected: const <Weather>{Weather.rainy},
  onSelectionChanged: (Set<Weather> newSelection) { },
  segments: const <ButtonSegment<Weather>>[
    ButtonSegment(
      icon: Icon(Icons.cloud_outlined),
      value: Weather.cloudy,
    ),
    ButtonSegment(
      icon: Icon(Icons.beach_access_sharp),
      value: Weather.rainy,
    ),
    ButtonSegment(
      icon: Icon(Icons.brightness_5_sharp),
      value: Weather.sunny,
    ),
  ],
),

Consulta la muestra completa sobre la migración de ToggleButtons a SegmentedButton.

Nuevos componentes

#
  • "Las barras de menú y menús en cascada" proporcionan un sistema de menú estilo escritorio que es totalmente navegable con el mouse o teclado. Los menús están anclados por un MenuBar o un MenuAnchor. El nuevo sistema de menú no es algo a lo que las aplicaciones existentes deban migrar obligatoriamente, sin embargo las aplicaciones que están desplegadas en la web o en plataformas de escritorio deberían considerar usarlo en lugar de las clases PopupMenuButton (y relacionadas).
  • DropdownMenu combina un campo de texto y un menú para producir lo que a veces se llama un combo box. Los usuarios pueden seleccionar un elemento de menú de una lista potencialmente grande ingresando una cadena coincidente o interactuando con el menú mediante pantalla táctil, ratón o teclado. Esto puede ser un buen reemplazo para el widget DropdownButton, aunque no es obligatorio.
  • SearchBar y SearchAnchor son para interacciones en las que el usuario introduce una consulta de búsqueda, la aplicación calcula una lista de respuestas coincidentes y luego el usuario selecciona una o ajusta la consulta.
  • Badge decora su hijo con una etiqueta pequeña de solo unos pocos caracteres, como '+1'. Las insignias se utilizan normalmente para decorar el icono dentro de una NavigationDestination, una NavigationRailDestination, una NavigationDrawerDestination o el icono de un botón, como en TextButton.icon.
  • FilledButton y FilledButton.tonal son muy similares a un ElevatedButton sin los cambios de elevación y sombra paralela.
  • FilterChip.elevated, ChoiceChip.elevated y ActionChip.elevated son variantes elevadas de los mismos chips con una sombra paralela y un color de relleno.
  • Dialog.fullscreen llena toda la pantalla y típicamente contiene un título, un botón de acción y un botón de cierre en la parte superior.

Timeline

#

En versión estable: 3.16

Referencias

#

Documentación:

Documentación de la API:

Issues relevantes:

PRs relevantes: