Descubre los patrones y registros de Dart

1. Introducción

Dart 3 incorpora patrones del lenguaje, una categoría nueva e importante de gramática. Además de esta nueva forma de escribir código de Dart, también se incluyen varias mejoras del lenguaje, como los registros para empaquetar datos de distinto tipo, los modificadores de clase para controlar los accesos y nuevas expresiones switch y sentencias if-case.

Estas funciones expanden las opciones disponibles a la hora de escribir código de Dart. En este codelab, aprenderás a usar dichas funciones para hacer que tu código resulte más compacto, simple y flexible.

En este codelab, se asume que conoces Flutter y Dart, aunque no es algo necesario. Antes de empezar, considera repasar los conceptos básicos con los siguientes recursos:

Qué compilarás

En este codelab, crearás una aplicación que muestre un documento JSON en Flutter. La aplicación simula datos en formato JSON provenientes de una fuente externa. El JSON contiene datos del documento, como la fecha de modificación, el título, los encabezados y los párrafos. Escribirás código para empaquetar de forma prolija los datos en registros de modo que se puedan transferir y desempaquetar en la ubicación que tus widgets de Flutter necesiten.

Luego, usarás patrones para compilar el widget adecuado cuando el valor coincida con ese patrón. También verás cómo usar los patrones para desestructurar datos en variables locales.

La aplicación final que compilarás en este codelab, un documento con un título, la fecha de la última modificación, los encabezados y los párrafos.

Qué aprenderás

  • Cómo crear un registro que almacena varios valores con diferentes tipos
  • Cómo mostrar varios valores a partir de una función que usa un registro
  • Cómo usar patrones para hacer coincidir, validar y desestructurar datos de registros y otros objetos
  • Cómo vincular valores coincidentes con patrones a variables nuevas o existentes
  • Cómo usar las nuevas funciones de las declaraciones switch, las expresiones switch y las sentencias if-case
  • Cómo aprovechar la verificación exhaustiva para garantizar que todos los casos sean controlados por una sentencia o expresión switch

2. Configura tu entorno

  1. Instala el SDK de Flutter.
  2. Configura un editor como Visual Studio Code (VS Code).
  3. Realiza los pasos de la configuración de la plataforma para, al menos, una plataforma de segmentación (iOS, Android, computadora de escritorio o navegador web).

3. Crea el proyecto

Antes de concentrarte en patrones, registros y otras funciones nuevas, dedica un momento a configurar tu entorno y el proyecto de Flutter simple para el que escribirás el código.

Obtén Dart

  • Para asegurarte de que estás usando Dart 3, ejecuta los siguientes comandos:
flutter channel stable
flutter upgrade
dart --version # This should print "Dart SDK version: 3.0.0" or higher

Crea un proyecto de Flutter

  1. Usa el comando flutter create para crear un nuevo proyecto llamado patterns_codelab. La marca --empty previene la creación de la app estándar de recuentos en el archivo lib/main.dart, que igualmente tendrías que quitar.
flutter create --empty patterns_codelab
  1. Luego, abre el directorio patterns_codelab con VS Code.
code patterns_codelab

Una captura de pantalla de VS Code que muestra el proyecto creado con el comando "flutter create".

Establece la versión mínima del SDK

  • Establece la restricción de la versión del SDK para tu proyecto de modo que se requiera Dart 3 o una versión posterior.

pubspec.yaml

environment:
  sdk: ^3.0.0

4. Configura el proyecto

En este paso, crearás los siguientes dos archivos Dart:

  • El archivo main.dart, que contiene widgets para la app
  • El archivo data.dart, que brinda los datos de la app

Define los datos para la app

  • Crea un archivo nuevo, lib/data.dart, y agrégale el siguiente código:

lib/data.dart

import 'dart:convert';

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);
}

const documentJson = '''
{
  "metadata": {
    "title": "My Document",
    "modified": "2023-05-10"
  },
  "blocks": [
    {
      "type": "h1",
      "text": "Chapter 1"
    },
    {
      "type": "p",
      "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    },
    {
      "type": "checkbox",
      "checked": false,
      "text": "Learn Dart 3"
    }
  ]
}
''';

Imagina un programa que recibe datos de una fuente externa, como una transmisión de E/S o una solicitud HTTP. En este codelab, simplificarás ese caso de uso más realista simulando datos JSON entrantes con una cadena de varias líneas en la variable documentJson.

Los datos en formato JSON se definen en la clase Document, y, más adelante en este codelab, agregarás funciones que muestren datos del JSON analizado. Esta clase define y, además, inicializa el campo _json en su constructor.

Ejecuta la app

El comando flutter create crea el archivo lib/main.dart como parte de la estructura de archivos de Flutter predeterminada.

  1. Con el objetivo de crear un punto de partida para la aplicación, reemplaza el contenido de main.dart con el siguiente código:

lib/main.dart

import 'package:flutter/material.dart';

import 'data.dart';

void main() {
 runApp(const DocumentApp());
}

class DocumentApp extends StatelessWidget {
 const DocumentApp({super.key});

 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     theme: ThemeData(useMaterial3: true),
     home: DocumentScreen(
       document: Document(),
     ),
   );
 }
}

class DocumentScreen extends StatelessWidget {
 final Document document;

 const DocumentScreen({
   required this.document,
   Key? key,
 }) : super(key: key);

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('Title goes here'),
     ),
     body: Column(
       children: [
         Center(
           child: Text('Body goes here'),
         ),
       ],
     ),
   );
 }
}

Agregaste a la app los dos widgets que se indican a continuación:

  • DocumentApp establece la versión más reciente de Material Design para la creación de temas de la IU.
  • DocumentScreen brinda el diseño visual de la página con el widget Scaffold.
  1. Para asegurarte de que todo funcione correctamente, ejecuta la app en tu máquina anfitrión y haz clic en Run and Debug:

Una imagen del botón "Run and Debug", disponible en la sección "Run and Debug" de la barra de actividades que se encuentra en el costado izquierdo.

  1. De forma predeterminada, Flutter elige la plataforma de segmentación que esté disponible. Para cambiar esa plataforma, selecciona la actual en la barra de estado:

Una captura de pantalla del selector de la plataforma de segmentación en VS Code.

Deberías ver un marco vacío con los elementos title y body definidos en el widget DocumentScreen:

Captura de pantalla de la aplicación compilada en este paso.

5. Crea y muestra registros

En este paso, usarás registros para mostrar varios valores a partir de una llamada a función. Luego, llamarás a esa función en el widget DocumentScreen para acceder a los valores y reflejarlos en la IU.

Crea y muestra un registro

  • En data.dart, agrega una nueva función a la clase Document llamada getMetadata que muestra un registro:

lib/data.dart

(String, {DateTime modified}) getMetadata() {
  var title = "My Document";
  var now = DateTime.now();

  return (title, modified: now);
}

El tipo de datos que se muestra para esta función es un registro con dos campos, uno con el tipo String y el otro con el tipo DateTime.

La sentencia return construye un nuevo registro encerrando los dos valores entre paréntesis, (title, modified: now).

El primer campo es posicional y no tiene nombre, y el segundo se llama modified.

Accede a campos de registros

  1. En el widget DocumentScreen, llama a getMetadata() en el método build para que puedas obtener tu registro y acceder a sus valores:

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var metadataRecord = document.getMetadata();

    return Scaffold(
      appBar: AppBar(
        title: Text(metadataRecord.$1),
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified ${metadataRecord.modified}',
            ),
          ),
        ],
      ),
    );
  }

La función getMetadata() muestra un registro, que se asigna a la variable local metadataRecord. Los registros son una forma simple y fácil de mostrar varios valores a partir de una única llamada a función y asignarlos a una variable.

Para acceder a los campos individuales compuestos en ese registro, puedes usar la sintaxis integrada de método get del registro.

  • Para obtener un campo posicional (un campo sin nombre, como title), usa el método get $<num> en el registro. Esta acción muestra solo campos sin nombre.
  • Los campos con nombre como modified no tienen un método get posicional, por lo que puedes usar su nombre directamente, como metadataRecord.modified.

Para determinar el nombre del método get de un campo posicional, comienza en $1 y omite los campos con nombre. Por ejemplo:

var record = (named: ‘v', ‘y', named2: ‘x', ‘z');
print(record.$1); // prints y
print(record.$2) // prints z
  1. Haz una recarga en caliente para ver los valores JSON que se muestran en la app. El complemento de Dart de VS Code hace la recarga en caliente cada vez que guardas un archivo.

Captura de pantalla de la app, que muestra el título y la fecha de modificación.

Puedes ver que cada campo efectivamente mantuvo su tipo.

  • El método Text() toma una cadena como su primer argumento.
  • El campo modified es de tipo DateTime y se convierte en una String usando la interpolación de cadenas.

La otra manera con seguridad de tipo para mostrar diferentes tipos de datos es definir una clase, algo mucho más detallado.

6. Determina coincidencias y desestructura con patrones

Los registros pueden recopilar de forma eficaz diferentes tipos de datos y pasarlos con facilidad. Ahora, mejora tu código con patrones.

Un patrón representa una estructura que un valor o más pueden tomar, como un plano. Los patrones se comparan con los valores reales para determinar si coinciden.

Algunos patrones, cuando coinciden, desestructuran el valor coincidente extrayendo datos de él. La desestructuración te permite desempaquetar valores de un objeto para asignarlos a variables locales o para establecer más coincidencias.

Desestructura un registro en variables locales

  1. Refactoriza el método build de DocumentScreen para llamar a getMetadata() y usar esta función para inicializar una declaración de variable de patrón:

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var (title, :modified) = document.getMetadata(); // New

    return Scaffold(
      appBar: AppBar(
        title: Text(title), // New
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified $modified', // New
            ),
          ),
        ],
      ),
    );
  }

El patrón del registro (title, :modified) contiene dos patrones de variables que coinciden con los campos del registro que muestra getMetadata().

  • La expresión coincide con el subpatrón porque el resultado es un registro con dos campos, uno de los cuales se llama modified.
  • Dado que coinciden, el patrón de la declaración de variables desestructura la expresión, accediendo a sus valores y vinculándolos a nuevas variables locales del mismo tipo y nombre, String title y DateTime modified.

La sintaxis del patrón de variable :modified es una abreviatura de modified: modified. Si quieres una nueva variable local con un nombre diferente, puedes escribir modified: localModified en su lugar.

  1. Haz una recarga en caliente para ver el mismo resultado del paso anterior. El comportamiento es exactamente igual, solo que hiciste que tu código fuera más conciso.

7. Usa patrones para extraer datos

En determinados contextos, los patrones no solo determinan coincidencias y desestructuran, sino que también pueden tomar una decisión acerca de lo que el código hace, según si el patrón coincide o no. A estos se los llama patrones refutables.

El patrón de declaración de variables que usaste en el último paso es un patrón irrefutable: el valor debe coincidir con el patrón o, de lo contrario, se generará un error y no sucederá la desestructuración. Considera cualquier declaración o asignación de variables: no puedes asignar un valor a una variable si no son del mismo tipo.

En cambio, los patrones refutables se usan en contextos de flujos de control y tienen las siguientes características:

  • Estos patrones esperan que algunos valores comparados no coincidan.
  • Su objetivo es influenciar el flujo de control en función de si el valor coincide o no.
  • Si no hay coincidencia, no interrumpen la ejecución con un error, solo avanzan a la sentencia que sigue.
  • Pueden desestructurar y vincular variables que solo se pueden usar cuando hay coincidencias.

Lee valores JSON sin patrones

En esta sección, leerás datos sin coincidencia de patrones para ver cómo los patrones te ayudan a trabajar con datos JSON.

  • Reemplaza la versión anterior de getMetadata() con una que lea los valores del mapa de _json. Copia y pega esta versión de getMetadata() en la clase Document:

lib/data.dart

(String, {DateTime modified}) getMetadata() {
  if (_json.containsKey('metadata')) {
    var metadataJson = _json['metadata'];
    if (metadataJson is Map) {
      var title = metadataJson['title'] as String;
      var localModified = DateTime.parse(metadataJson['modified'] as String);
      return (title, modified: localModified);
    }
  }
  throw const FormatException('Unexpected JSON');
}

Este código valida que los datos se hayan estructurado de forma correcta sin usar patrones. En un paso posterior, usarás la coincidencia de patrones para realizar la misma validación con menos código. Esto realiza tres verificaciones antes de hacer cualquier otra cosa:

  • Que el archivo JSON contenga la estructura de los datos que esperas: if (_json.containsKey('metadata'))
  • Que los datos tengan el tipo que esperas: if (metadataJson is Map)
  • Que los datos no sean nulos, lo que se confirma implícitamente con la verificación anterior

Lee valores JSON usando un patrón de mapas

Sin un patrón refutable, puedes verificar que el JSON tenga la estructura esperada usando un patrón de mapas.

  • Reemplaza la versión anterior de getMetadata() con este código:

lib/data.dart

  (String, {DateTime modified}) getMetadata() {
    if (_json
        case {
          'metadata': {
            'title': String title,
            'modified': String localModified,
          }
        }) {
      return (title, modified: DateTime.parse(localModified));
    } else {
      throw const FormatException('Unexpected JSON');
    }
  }

Aquí, verás una nueva clase de sentencia (presentada en Dart 3): la if-case. El cuerpo de case solo se ejecutará si su patrón coincide con los datos en _json. Esta coincidencia realiza las mismas verificaciones que escribiste en la primera versión de getMetadata() para validar los datos JSON entrantes. Este código valida lo siguiente:

  • _json es de tipo mapa.
  • _json contiene una clave metadata.
  • _json no es nulo.
  • _json['metadata'] también es de tipo mapa.
  • _json['metadata'] contiene las claves title y modified.
  • title y localModified son cadenas no nulas.

Si el valor no coincide, el patrón refuta (rechaza continuar con la ejecución) y procede con la cláusula else. Si la coincidencia es exitosa, el patrón desestructura los valores de title y modified del mapa, y los vincula a variables locales nuevas.

Para obtener una lista completa de patrones, consulta la tabla de la sección Patrones de la especificación de la función.

8. Prepara la app para más patrones

Hasta el momento, te concentraste en la parte de los metadata de los datos JSON. En este paso, refinarás tu lógica empresarial un poco más para controlar los datos de la lista de blocks y renderizarlos en tu app.

{
  "metadata": {
    // ...
  },
  "blocks": [
    {
      "type": "h1",
      "text": "Chapter 1"
    },
    // ...
  ]
}

Crea una clase que almacene datos

  • Agrega una clase nueva, Block, en data.dart, que se use para leer y escribir datos para uno de los bloques de datos JSON.

lib/data.dart

class Block {
  final String type;
  final String text;
  Block(this.type, this.text);

  factory Block.fromJson(Map<String, dynamic> json) {
    if (json case {'type': var type, 'text': var text}) {
      return Block(type, text);
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }
}

El constructor de fábrica fromJson() usa la misma sentencia if-case con el patrón de mapas que viste antes.

Observa que el elemento json coincide con el patrón de mapas, aunque una de las claves, checked, no se representa en el patrón. Los patrones de mapas ignoran toda entrada del objeto de mapa que no se represente de forma explícita en el patrón.

Muestra una lista de objetos Block

  • A continuación, agrega una nueva función, getBlocks(), a la clase Document. getBlocks() analiza el JSON en instancias de la clase Block y muestra una lista de bloques para renderizar en tu IU:

lib/data.dart

  List<Block> getBlocks() {
    if (_json case {'blocks': List blocksJson}) {
      return <Block>[
        for (var blockJson in blocksJson) Block.fromJson(blockJson)
      ];
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }

La función getBlocks() muestra una lista de objetos Block, que luego usarás para compilar la IU. Una sentencia if-case conocida valida y transmite el valor de los metadatos de blocks en una nueva List llamada blocksJson (sin patrones, necesitarías el método toList() para hacer la transmisión).

El literal de la lista contiene una colección para completar la lista nueva con objetos Block.

En esta sección, no se presenta ninguna función relacionada con patrones que no hayas probado en este codelab. En el siguiente paso, prepararás la renderización de los elementos de lista en tu IU.

9. Usa patrones para mostrar el documento

Ya desestructuraste y volviste a componer con éxito los datos JSON con una sentencia if-case y patrones refutables. Sin embargo, la sentencia if-case solo es una de las mejoras sobre estructuras de flujo de control que incluyen los patrones. Ahora, aplicarás tus conocimientos sobre patrones refutables para sentencias switch.

Controla lo que se renderiza usando patrones con sentencias switch

  • En main.dart, crea un widget nuevo, BlockWidget, que determine el estilo de cada bloque con base en su campo type.

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    TextStyle? textStyle;
    switch (block.type) {
      case 'h1':
        textStyle = Theme.of(context).textTheme.displayMedium;
      case 'p' || 'checkbox':
        textStyle = Theme.of(context).textTheme.bodyMedium;
      case _:
        textStyle = Theme.of(context).textTheme.bodySmall;
    }

    return Container(
      margin: const EdgeInsets.all(8),
      child: Text(
        block.text,
        style: textStyle,
      ),
    );
  }
}

La sentencia switch del método build cambia el campo type del objeto block.

  1. La primera sentencia case usa un patrón de cadena constante. El patrón coincidirá si block.type es igual al valor constante h1.
  2. La segunda sentencia case usa un patrón de operación lógica OR con dos patrones de cadena constantes como subpatrones. El patrón coincidirá si block.type coincide con cualquiera de los subpatrones p o checkbox.
  1. La última sentencia case es un patrón de comodín, _. Los comodines en esta sentencia switch coinciden con todo lo demás. Se comportan de la misma manera que las cláusulas default, que se admiten en las sentencias switch (solo que son un poco más detalladas).

Los patrones de comodín se pueden usar siempre que se admita un patrón, por ejemplo, en un patrón de declaración de variables: var (title, _) = document.getMetadata();

En este contexto, el comodín no vincula ninguna variable. Descarta el segundo campo.

En la siguiente sección, aprenderás más funciones switch luego de mostrar los objetos Block.

Muestra el contenido del documento

Crea una variable local que contenga la lista de objetos Block llamando a getBlocks() en el método build del widget de DocumentScreen.

  1. Reemplaza el método build existente en DocumentationScreen con esta versión:

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var (title, :modified) = document.getMetadata();
    var blocks = document.getBlocks(); // New

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          // New
          Text('Last modified: $modified'),
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) {
                return BlockWidget(block: blocks[index]);
              },
            ),
          ),
        ],
      ),
    );
  }

La línea BlockWidget(block: blocks[index]) construye un widget de BlockWidget por cada elemento de la lista de bloques que muestra el método getBlocks().

  1. Ejecuta la aplicación. Luego, deberías ver los bloques aparecer en pantalla:

Captura de pantalla de la app que muestra contenido de la sección "blocks" de los datos JSON.

10. Usa expresiones switch

Los patrones agregan muchas funciones a switch y case. Para poder usarlos en más lugares, Dart tiene las expresiones switch. Una serie de sentencias case puede brindar un valor directamente a una asignación de variable o sentencia return.

Convierte la sentencia switch en una expresión switch

El analizador de Dart provee ayuda para que puedas hacer cambios a tu código.

  1. Mueve el cursor a la sentencia switch de la sección anterior.
  2. Haz clic en la bombilla para ver la ayuda disponible.
  3. Selecciona la opción Convert to switch expression.

Captura de pantalla de la opción "Convert to switch expression" disponible en VS Code.

La nueva versión del código tiene el siguiente aspecto:

TextStyle? textStyle;
textStyle = switch (block.type) {
  'h1' => Theme.of(context).textTheme.displayMedium,
  'p' || 'checkbox' => Theme.of(context).textTheme.bodyMedium,
  _ => Theme.of(context).textTheme.bodySmall
};

Una expresión switch se parece a una sentencia switch, pero elimina la palabra clave case y usa => para separar el patrón del cuerpo de la sentencia case. A diferencia de las sentencias switch, las expresiones switch muestran un valor y pueden usarse en cualquier lugar donde pueda usarse una expresión.

11. Usa patrones de objeto

Dart es un lenguaje orientado a objetos, de modo que los patrones aplican a todos los objetos. En este paso, emplearás un switch en un patrón de objeto y desestructurarás las propiedades de objeto para mejorar la lógica de renderización de fecha de tu IU.

Extrae propiedades de patrones de objeto

En esta sección, mejorarás la visualización de la fecha de última modificación usando patrones.

  • Agrega el método formatDate a main.dart:

lib/main.dart

String formatDate(DateTime dateTime) {
  var today = DateTime.now();
  var difference = dateTime.difference(today);

  return switch (difference) {
    Duration(inDays: 0) => 'today',
    Duration(inDays: 1) => 'tomorrow',
    Duration(inDays: -1) => 'yesterday',
    Duration(inDays: var days, isNegative: true) => '${days.abs()} days ago',
    Duration(inDays: var days) => '$days days from now',
  };
}

Este método muestra una expresión switch que cambia en función del valor difference, un objeto Duration. Representa el período entre today y el valor modified de los datos JSON.

Cada sentencia case de la expresión switch usa un patrón de objeto que establece una coincidencia llamando a métodos get en las propiedades inDays y isNegative del objeto. La sintaxis parece que estuviera construyendo un objeto de duración, pero en realidad está accediendo a campos del objeto difference.

Las primeras tres sentencias case usan subpatrones de las constantes 0, 1 y -1 para hacer coincidir la propiedad inDays del objeto y mostrar la cadena correspondiente.

Las últimas dos sentencias case controlan la duración más allá de hoy, ayer y mañana:

  • Si la propiedad isNegative coincide con el patrón de constante booleana true, lo que significa que la fecha de modificación es una fecha pasada, mostrará días atrás.
  • Si esa sentencia case no capta la diferencia, entonces la duración debe ser un número positivo de días (no hace falta verificarlo de forma explícita mediante isNegative: false), de modo que la fecha de modificación es una fecha futura, y se mostrará días a partir de ahora.

Agrega lógica de formato para las semanas

  • Agrega dos sentencias case a tu función de formato para identificar las duraciones mayores a 7 días de modo que la IU pueda mostrarlas como semanas:

lib/main.dart

String formatDate(DateTime dateTime) {
  var today = DateTime.now();
  var difference = dateTime.difference(today);

  return switch (difference) {
    Duration(inDays: 0) => 'today',
    Duration(inDays: 1) => 'tomorrow',
    Duration(inDays: -1) => 'yesterday',
    Duration(inDays: var days) when days > 7 => '${days ~/ 7} weeks from now', // New
    Duration(inDays: var days) when days < -7 => '${days.abs() ~/ 7} weeks ago', // New
      Duration(inDays: var days, isNegative: true) => '${days.abs()} days ago',
      Duration(inDays: var days) => '$days days from now',
  };
}

En este código, se presentan las cláusulas de guarda:

  • Una cláusula de guarda usa la palabra clave when luego de un patrón de sentencia case.
  • Pueden utilizarse en sentencias if-case, sentencias switch y expresiones switch.
  • Solo agregan una condición a un patrón luego de que se le establezca una coincidencia.
  • Si la cláusula de guarda da como resultado el valor falso, el patrón completo será refutado y la ejecución avanzará con la siguiente sentencia case.

Agrega la fecha con el nuevo formato a la IU

  1. Por último, actualiza el método build en DocumentScreen para usar la función formatDate:

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var (title, :modified) = document.getMetadata();
    var formattedModifiedDate = formatDate(modified); // New
    var blocks = document.getBlocks();

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          Text('Last modified: $formattedModifiedDate'), // New
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) =>
                BlockWidget(block: blocks[index]),
            ),
          ),
        ],
      ),
    );
  }
  1. Haz una recarga en caliente para ver los cambios en tu app:

Captura de pantalla de la app, que muestra el mensaje "Last modified: 2 weeks ago" usando la función formatDate().

12. Sella una clase para que ejecute sentencias switch exhaustivas

Observa que no usaste un comodín ni una sentencia case predeterminada al final de la última sentencia switch. A pesar de que se recomienda que siempre incluyas una sentencia case para los valores que no cumplan con las condiciones que establezcas, está bien si no lo incluyes en este ejemplo simple, ya que conoces las sentencias case que definiste y todos los valores posibles que inDays podría tomar.

Cuando se controlan todas las sentencias case dentro de una sentencia switch, a eso se lo llama sentencia switch exhaustiva. Por ejemplo, una sentencia switch sobre un valor de tipo bool es exhaustiva cuando tiene sentencias case para los valores true y false. Una sentencia switch sobre un valor de tipo enum es exhaustiva cuando tiene sentencias case para cada valor de tipo enum, ya que estos representan una cantidad fija de valores constantes.

Dart 3 extendió la verificación exhaustiva a jerarquías de objetos y clases con el nuevo modificador de clase sealed. Refactoriza tu clase Block como una superclase sellada.

Crea las subclases

  • En data.dart, crea tres clases nuevas, HeaderBlock, ParagraphBlock y CheckboxBlock, que extiendan Block:

lib/data.dart

class HeaderBlock extends Block {
  final String text;
  HeaderBlock(this.text);
}

class ParagraphBlock extends Block {
  final String text;
  ParagraphBlock(this.text);
}

class CheckboxBlock extends Block {
  final String text;
  final bool isChecked;
  CheckboxBlock(this.text, this.isChecked);
}

Cada una de estas clases corresponde a los diferentes valores de type del JSON original: 'h1', 'p' y 'checkbox'.

Sella la superclase

  • Marca la clase Block como sealed. Luego, refactoriza la sentencia if-case como una expresión switch que muestre la subclase que corresponde al type especificado en el JSON:

lib/data.dart

sealed class Block {
  Block();

  factory Block.fromJson(Map<String, Object?> json) {
    return switch (json) {
      {'type': 'h1', 'text': String text} => HeaderBlock(text),
      {'type': 'p', 'text': String text} => ParagraphBlock(text),
      {'type': 'checkbox', 'text': String text, 'checked': bool checked} =>
        CheckboxBlock(text, checked),
      _ => throw const FormatException('Unexpected JSON format'),
    };
  }
}

La palabra clave sealed es un modificador de clase que implica que puedes extender o implementar solo esta clase en la misma biblioteca. Dado que el analizador conoce los subtipos de esta clase, reportará un error si una sentencia switch no cubre uno de ellos y no es exhaustiva.

Usa una expresión switch para mostrar widgets

  1. Actualiza la clase BlockWidget en main.dart con una expresión switch que usa patrones de objeto para cada sentencia case:

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(8),
      child: switch (block) {
        HeaderBlock(:var text) => Text(
          text,
          style: Theme.of(context).textTheme.displayMedium,
        ),
        ParagraphBlock(:var text) => Text(text),
        CheckboxBlock(:var text, :var isChecked) => Row(
          children: [
            Checkbox(value: isChecked, onChanged: (_) {}),
            Text(text),
          ],
        ),
      },
    );
  }
}

En tu primera versión de BlockWidget, estableciste una sentencia switch en un campo de un objeto Block para que se muestre un TextStyle. Ahora, establecerás una sentencia switch para una instancia del objeto Block y lo harás coincidir con los patrones de objeto que representan sus subclases, a la vez que extraes las propiedades del objeto.

El analizador de Dart puede comprobar que cada subclase se controle en la expresión switch porque sellaste la clase Block.

También observa que, si usas una expresión switch aquí, podrás pasar el resultado directamente al elemento child, a diferencia de la sentencia return independiente que se necesitaba antes.

  1. Haz una recarga en caliente para ver que se renderizan los datos JSON de la casilla de verificación por primera vez:

Captura de pantalla de la app que muestra la casilla de verificación "Learn Dart 3".

13. Felicitaciones

Probaste con éxito trabajar con patrones, registros, sentencias switch y case mejoradas, y clases selladas. Viste una gran cantidad de información, pero este es solo el comienzo para estas funciones. Para obtener más información, consulta la especificación de la función.

Los distintos tipos de patrón, los contextos en los que pueden aparecer y el posible anidamiento de subpatrones hacen que los comportamientos posibles parezcan ilimitados. Pero son fáciles de ver.

Puedes imaginar todo tipo de maneras de mostrar contenido en Flutter usando patrones. Con ellos, puedes extraer datos de forma segura para compilar tu IU en pocas líneas de código.

¿Qué sigue?

  • Consulta la documentación sobre patrones, registros, sentencias switch y case mejoradas, y modificadores de clase en la sección Idioma de la documentación de Dart.

Documentos de referencia

Consulta el ejemplo completo en su repositorio.

Para obtener especificaciones detalladas de cada función nueva, consulta los documentos de diseño originales: