Conheça os padrões e registros do Dart

1. Introdução

O Dart 3 introduz padrões na linguagem, uma nova categoria importante de gramática. Além dessa nova maneira de escrever código Dart, há vários outras melhorias na linguagem, incluindo registros para agrupar dados de diferentes tipos, modificadores de classe para controlar o acesso e novas expressões switch e instruções if-case.

Esses recursos aumentam suas opções ao escrever código Dart. Neste codelab, você vai aprender a usá-los para tornar seu código mais compacto, simplificado e flexível.

Este codelab presume que você tenha alguma familiaridade com o Flutter e o Dart, embora isso não seja necessário. Antes de começar, revise os conceitos básicos com os seguintes recursos:

O que você vai criar

Este codelab cria um aplicativo que mostra um documento JSON no Flutter. O aplicativo simula o JSON recebido de uma origem externa. O JSON contém dados do documento, como data da modificação, título, cabeçalhos e parágrafos. Você vai escrever código para empacotar dados de forma organizada em registros para que possam ser transferidos e desempacotados sempre que os widgets do Flutter precisarem deles.

Em seguida, você vai usar padrões para criar o widget apropriado quando o valor corresponder a esse padrão. Você também vai aprender a usar padrões para desestruturar dados em variáveis locais.

O aplicativo final que você vai criar neste codelab será um documento com título, data da última modificação, cabeçalhos e parágrafos.

O que você vai aprender

  • Como criar um registro que armazene vários valores com tipos diferentes.
  • Como retornar diversos valores de uma função usando um registro.
  • Como usar padrões para corresponder, validar e desestruturar dados de registros e outros objetos.
  • Como vincular valores correspondentes a padrões a variáveis novas ou existentes.
  • Como usar os novos recursos de instrução switch, expressões switch e instruções if-case.
  • Como usar a verificação de exaustão para garantir que todos os casos sejam tratados em uma instrução ou expressão switch.

2. Configurar o ambiente

  1. Instale o SDK do Flutter.
  2. Configure um editor, como o Visual Studio Code (VS Code).
  3. Siga as etapas de configuração para pelo menos uma plataforma segmentada (iOS, Android, computador ou navegador da Web).

3. Criar o projeto

Antes de mergulhar nos padrões, registros e outros novos recursos, reserve um momento para configurar seu ambiente e o projeto simples do Flutter para o qual você vai escrever todo o código.

Instalar o Dart

  • Para garantir que está usando o Dart 3, execute os seguintes comandos:
flutter channel stable
flutter upgrade
dart --version # This should print "Dart SDK version: 3.0.0" or higher

Criar um projeto do Flutter

  1. Use o comando flutter create para criar um novo projeto chamado patterns_codelab. A flag --empty impede a criação do app contador padrão no arquivo lib/main.dart que, de qualquer maneira, precisaria ser removido.
flutter create --empty patterns_codelab
  1. Depois, abra o diretório patterns_codelab usando o VS Code.
code patterns_codelab

Uma captura de tela do VS Code mostrando o projeto criado com o comando "flutter create".

Definir a versão mínima do SDK

  • Defina a restrição de versão do SDK para que seu projeto dependa do Dart 3 ou mais recente.

pubspec.yaml (link em inglês)

environment:
  sdk: ^3.0.0

4. Configurar o projeto

Nesta etapa, você vai criar dois arquivos Dart:

  • O arquivo main.dart que contém widgets para o app.
  • O arquivo data.dart que fornece os dados do app.

Definir os dados para o app

  • Crie um novo arquivo, lib/data.dart, e adicione o seguinte 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"
    }
  ]
}
''';

Imagine um programa que receba dados de uma fonte externa, como um stream E/S ou solicitação HTTP. Neste codelab, você vai simplificar esse caso de uso mais realista simulando dados JSON de entrada com uma string de várias linhas na variável documentJson.

Os dados JSON são definidos na classe Document e, mais adiante neste codelab, você vai adicionar funções que retornam dados do JSON analisado. Essa classe define e inicializa o campo _json no construtor.

Executar o app

O comando flutter create cria o arquivo lib/main.dart como parte da estrutura de arquivos padrão do Flutter.

  1. Para criar um ponto de partida para o aplicativo, substitua o conteúdo de main.dart pelo seguinte 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'),
         ),
       ],
     ),
   );
 }
}

Você adicionou os dois widgets a seguir ao app:

  • O DocumentApp configura a versão mais recente do Material Design para aplicação de temas na interface.
  • O DocumentScreen fornece o layout visual da página usando o widget Scaffold.
  1. Para garantir que tudo esteja funcionando sem problemas, execute o app em sua máquina host clicando em Run and Debug:

Uma imagem do botão "Run and debug", disponível na seção de mesmo nome da barra de atividade do lado esquerdo.

  1. Por padrão, o Flutter escolhe a plataforma segmentada que estiver disponível. Para mudar, selecione a plataforma atual na barra de status:

Uma captura de tela do seletor da plataforma segmentada no VS Code.

Um frame vazio será exibido com os elementos title e body definidos no widget DocumentScreen:

Uma captura de tela do aplicativo criado nesta etapa.

5. Criar e retornar registros

Nesta etapa, você usa registros para retornar vários valores de uma chamada de função. Em seguida, você chama essa função no widget DocumentScreen para acessar os valores e refleti-los na interface.

Criar e retornar um registro

  • Em data.dart, adicione uma nova função à classe de documentos chamada getMetadata, que retorna um registro:

lib/data.dart

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

  return (title, modified: now);
}

O tipo de retorno para essa função é um registro com dois campos, um com o tipo String e outro com DateTime.

A instrução de retorno cria um novo registro colocando os dois valores entre parênteses, (title, modified: now).

O primeiro campo é de posicionamento e sem nome, e o segundo tem o nome modified.

Acessar campos de registro

  1. No widget DocumentScreen, chame getMetadata() no método build para que você possa receber seu registro e acessar os 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}',
            ),
          ),
        ],
      ),
    );
  }

A função getMetadata() retorna um registro, que é atribuído à variável local metadataRecord. Registros são uma maneira leve e fácil de retornar diversos valores de uma única chamada de função e atribuí-los a uma variável.

Para acessar os campos individuais compostos nesse registro, é possível usar a sintaxe getter integrada dos registros.

  • Para acessar um campo posicional (sem nome, como title), use o getter $<num> no registro. Isso retorna apenas campos sem nome.
  • Campos nomeados, como modified, não têm um getter posicional, portanto você pode usar o nome diretamente, como metadataRecord.modified.

Para determinar o nome de um getter para um campo posicional, comece em $1 e ignore os campos nomeados. Exemplo:

var record = (named: ‘v', ‘y', named2: ‘x', ‘z');
print(record.$1); // prints y
print(record.$2) // prints z
  1. Faça a recarga automática para verificar os valores JSON mostrados no app. O plugin Dart do VS Code executa a recarga automática sempre que você salva um arquivo.

Uma captura de tela do app, que mostra o título e a data da modificação.

Você pode verificar que cada campo, de fato, manteve o próprio tipo.

  • O método Text() recebe uma string como primeiro argumento.
  • O campo modified é um DateTime e é convertido em uma String usando interpolação de strings.

A outra maneira segura de retornar diferentes tipos de dados é definir uma classe, o que é mais detalhado.

6. Combinar e desestruturar com padrões

Os registros podem coletar com eficiência diferentes tipos de dados e transmiti-los com facilidade. Agora, melhore seu código usando padrões.

Um padrão representa uma estrutura que um ou mais valores podem assumir, como uma planta. Os padrões são comparados com os valores reais para determinar se eles são correspondentes.

Em caso positivo, alguns padrões desestruturam o valor correspondente, extraindo dados dele. A desestruturação permite que você desempacote os valores de um objeto para atribuí-los a variáveis locais ou realizar outras correspondências com eles.

Desestruturar um registro em variáveis locais

  1. Refatore o método build de DocumentScreen para chamar getMetadata() e use-o para inicializar uma declaração de variável padrão:

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
            ),
          ),
        ],
      ),
    );
  }

O padrão de registro (title, :modified) contém dois padrões variáveis que correspondem aos campos de registro retornados por getMetadata().

  • A expressão corresponde ao subpadrão porque o resultado é um registro com dois campos, um dos quais é denominado modified.
  • Como eles são correspondentes, o padrão de declaração de variável desestrutura a expressão, acessando os valores e vinculando-os a novas variáveis locais dos mesmos tipos e nomes, String title e DateTime modified.

A sintaxe do padrão variável :modified é uma abreviação de modified: modified. Se você quiser uma nova variável local com um nome diferente, pode escrever modified: localModified.

  1. Faça a recarga automática para ter o mesmo resultado que na etapa anterior. O comportamento é exatamente o mesmo. Você só tornou o código mais conciso.

7. Usar padrões para extrair dados

Em determinados contextos, os padrões não apenas correspondem e desestruturam, mas também podem decidir sobre o que o código faz, dependendo se o padrão corresponde ou não. Esses são chamados de padrões refutáveis.

O padrão de declaração de variável que você usou na última etapa é um padrão irrefutável: o valor precisa corresponder ao padrão. Caso contrário, ocorrerá um erro e a desestruturação não vai acontecer. Pense em qualquer declaração ou atribuição de variável. Não é possível atribuir um valor a uma variável se elas não forem do mesmo tipo.

Por outro lado, padrões refutáveis são usados em contextos de fluxo de controle:

  • Eles esperam que alguns valores comparados não correspondam.
  • Eles se destinam a influenciar o fluxo de controle, com base na correspondência ou não do valor.
  • Eles não interrompem a execução com um erro se não corresponderem, apenas passam para a próxima instrução.
  • Eles podem desestruturar e vincular variáveis que só podem ser usadas quando correspondem.

Ler valores JSON sem padrões

Nesta seção, você vai ler dados sem correspondência de padrão para conferir como os padrões podem ajudar a trabalhar com dados JSON.

  • Substitua a versão anterior de getMetadata() por uma que leia valores do mapa _json. Copie e cole essa versão de getMetadata() na classe 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');
}

Esse código valida que os dados estão estruturados corretamente sem usar padrões. Em uma etapa posterior, você usa a correspondência de padrões para realizar a mesma validação usando menos código. Ela executa três verificações antes de qualquer outra coisa:

  • Se o JSON contém a estrutura de dados que você espera: if (_json.containsKey('metadata'))
  • Se os dados têm o tipo que você espera: if (metadataJson is Map)
  • Se os dados não são nulos, o que é confirmado de forma implícita na verificação anterior.

Ler valores JSON usando um padrão de mapa

Com um padrão refutável, você pode verificar se o JSON tem a estrutura esperada usando um padrão de mapa.

  • Substitua a versão anterior de getMetadata() por 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');
    }
  }

Aqui está um novo tipo de instrução if (introduzida no Dart 3), a if-case. O corpo do caso só é executado se o padrão corresponder aos dados em _json. Essa correspondência realiza as mesmas verificações que você escreveu na primeira versão de getMetadata() para validar o JSON de entrada. Esse código valida o seguinte:

  • _json é do tipo mapa.
  • _json contém uma chave metadata.
  • _json não é nulo.
  • _json['metadata'] também é do tipo mapa.
  • _json['metadata'] contém as chaves title e modified.
  • title e localModified são strings e não são nulos.

Se o valor não corresponder, o padrão refuta (se recusa a continuar a execução) e prossegue para a cláusula else. Se não houver correspondência, o padrão desestrutura os valores de title e modified do mapa e os vincula a novas variáveis locais.

Para uma lista completa de padrões, consulte a tabela na seção Padrões da especificação de recursos.

8. Preparar o app para mais padrões

Até agora, você abordou a parte de metadata dos dados JSON. Nesta etapa, você vai refinar um pouco mais sua lógica de negócios para tratar os dados da lista blocks e renderizá-los no app.

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

Criar uma classe que armazena dados

  • Adicione uma nova classe, Block, a data.dart, que é usada para ler e armazenar os dados de um dos blocos de dados 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');
    }
  }
}

O construtor de fábrica fromJson() usa a mesma instrução if-case com um padrão de mapa que você já usou antes.

Observe que o json corresponde ao padrão de mapa, embora uma das chaves, checked, não seja considerada no padrão. Os padrões de mapa ignoram as entradas no objeto de mapa que não sejam explicitamente consideradas no padrão.

Retornar uma lista de objetos Block

  • Em seguida, adicione uma nova função, getBlocks(), à classe Document. getBlocks() analisa o JSON em instâncias da classe Block e retorna uma lista de blocos para renderizar na sua interface:

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');
    }
  }

A função getBlocks() retorna uma lista de objetos Block, que você vai usar depois para criar a interface. Uma instrução if-case conhecida executa a validação e converte o valor dos metadados blocks em uma nova List chamada blocksJson. Sem padrões, você precisaria do método toList() para converter.

O literal da lista contém uma collection for para preencher a nova lista com objetos Block.

Esta seção não apresenta nenhum recurso relacionado a padrões que você ainda não tenha experimentado neste codelab. Na próxima etapa, você vai se preparar para renderizar os itens da lista em sua interface.

9. Usar padrões para mostrar o documento

Agora você já pode desestruturar e recompor seus dados JSON usando uma instrução if-case e padrões refutáveis. Mas a if-case é apenas uma das melhorias para controlar as estruturas de fluxo que acompanham os padrões. Agora, você vai aplicar seu conhecimento de padrões refutáveis para instruções switch.

Controlar o que é renderizado usando padrões com instruções switch

  • Em main.dart, crie um novo widget, BlockWidget, que determina o estilo de cada bloco baseado no 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,
      ),
    );
  }
}

A instrução switch no método build alterna o campo type do objeto block.

  1. A primeira instrução de caso usa um padrão de string constante. O padrão corresponde se block.type for igual ao valor de constante h1.
  2. A segunda instrução de caso usa um padrão "ou" lógico com dois padrões de strings constantes como subpadrões. O padrão corresponde se block.type for igual a um dos subpadrões p ou checkbox.
  1. O caso final é um padrão curinga, _. Curingas em casos de alternância correspondem a tudo. Eles se comportam da mesma maneira que cláusulas default, que ainda são permitidas em instruções switch. São apenas um pouco mais detalhados.

Padrões de curinga podem ser usados sempre que um padrão é permitido. Por exemplo, em um padrão de declaração variável: var (title, _) = document.getMetadata();

Nesse contexto, o curinga não vincula nenhuma variável. Ele descarta o segundo campo.

Na próxima seção, você vai saber mais sobre os recursos de switch depois de mostrar os objetos Block.

Mostrar o conteúdo do documento

Crie uma variável local que contenha a lista de objetos Block chamando getBlocks() no método build do widget DocumentScreen.

  1. Substitua o método build existente em DocumentationScreen por esta versão:

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]);
              },
            ),
          ),
        ],
      ),
    );
  }

A linha BlockWidget(block: blocks[index]) cria um widget BlockWidget para cada item na lista de blocos retornados do método getBlocks().

  1. Execute o aplicativo e os blocos vão aparecer na tela:

Captura de tela do app mostrando conteúdo da seção de blocos dos dados JSON.

10. Usar expressões switch

Padrões adicionam muitos recursos a switch e case. Para que eles possam ser usados em mais lugares, o Dart tem as expressões switch. Uma série de casos pode fornecer um valor diretamente a uma atribuição variável ou instrução de retorno.

Converter a instrução switch em uma expressão switch

O analisador Dart fornece assistências para ajudar você a fazer mudanças no seu código.

  1. Mova o cursor da seção anterior para a instrução switch.
  2. Clique na lâmpada para conferir as assistências disponíveis.
  3. Selecione a assistência Convert to switch expression.

Uma captura de tela da assistência "convert to switch expression" disponível em VS Code.

A nova versão desse código é parecida com o seguinte:

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

Uma expressão switch se parece com uma instrução switch, mas ela elimina a palavra-chave case e usa => para separar o padrão do corpo do caso. Diferente das instruções switch, as expressões switch retornam um valor e podem ser usadas em qualquer lugar em que uma expressão possa ser utilizada.

11. Usar padrões de objeto

Dart é uma linguagem orientada a objetos, portanto, os padrões se aplicam a todos os objetos. Nesta etapa, você vai alternar um padrão de objeto e desestruturar as propriedades de objeto para aprimorar a lógica de renderização de data da interface.

Extrair propriedades de padrões de objeto

Nesta seção, você vai melhorar como a data da última modificação é mostrada usando padrões.

  • Adicione o 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',
  };
}

Esse método retorna uma expressão switch que alterna no valor difference, um objeto de Duration. Ele representa o intervalo de tempo entre today e o valor modified dos dados JSON.

Cada caso da expressão switch está usando um padrão de objeto correspondente ao chamar getters nas propriedades inDays e isNegative do objeto. A sintaxe parece estar criando um objeto Duration, mas, na verdade, está acessando campos no objeto difference.

Os três primeiros casos usam subpadrões constantes 0, 1 e -1 para corresponder à propriedade do objeto inDays e retornar a string correspondente.

Os dois últimos casos lidam com durações além de hoje, ontem e amanhã:

  • Se a propriedade isNegative corresponder ao padrão booleano constante true, o que significa que a data da modificação foi no passado, ela vai mostrar dias atrás.
  • Se esse caso não detectar a diferença, a duração precisa ser um número positivo de dias. Não é necessário verificar explicitamente com isNegative: false. Portanto, a data de modificação está no futuro e mostra dias a partir de agora.

Adicionar lógica de formatação para semanas

  • Adicione dois novos casos à função de formatação para identificar durações superiores a sete dias, para que a interface possa mostrar 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',
  };
}

Este código introduz cláusulas de proteção:

  • Uma cláusula de proteção usa a palavra-chave when depois de um padrão de caso.
  • Elas podem ser usadas em if-cases, instruções e expressões switch.
  • Elas só adicionam uma condição a um padrão depois de uma correspondência.
  • Se a cláusula de proteção for avaliada como falsa, o padrão inteiro será refutado e a execução vai prosseguir para o próximo caso.

Adicionar a data recém formatada à interface

  1. Finalmente, atualize o método build na DocumentScreen para usar a função 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. Use a recarga automática para conferir as mudanças no seu app:

Uma captura de tela do app que mostra uma string "Última modificação: 2 semanas atrás" usando a função formatDate().

12. Selar uma classe para switch exaustiva

Observe que você não usou um curinga ou um caso padrão no fim do último switch. Embora seja uma boa prática sempre incluir um caso para valores que possam falhar, tudo bem não fazer isso em um exemplo simples como esse, já que você sabe que os casos definidos representam todos os valores possíveis que inDays poderia assumir.

Quando todos os casos em um switch são tratados, isso é chamado de switch exaustivo. Por exemplo, o switch em um tipo bool é exaustivo quando tem casos para true e false. Switch do tipo enum é exaustivo quando há casos para cada um dos valores do tipo enumerado, também porque eles representam um número fixo de valores constantes.

O Dart 3 ampliou a verificação de exaustão para objetos e hierarquias de classe com o novo modificador de classe sealed. Refatore sua classe Block como uma superclasse selada.

Criar as subclasses

  • Em data.dart, crie três novas classes, HeaderBlock, ParagraphBlock e CheckboxBlock, que estendem 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 uma dessas classes corresponde a valores type diferentes do JSON original: 'h1', 'p' e 'checkbox'.

Selar a superclasse

  • Marque a classe Block como sealed. Depois, refatore a if-case como uma expressão switch que retorna a subclasse correspondente ao type especificado no 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'),
    };
  }
}

A palavra-chave sealed é um modificador de classe que significa que você só pode estender ou implementar essa classe na mesma biblioteca. Como o analisador conhece os subtipos dessa classe, ele vai informar um erro se uma opção não abranger um deles e não for exaustiva.

Usar uma expressão switch para mostrar widgets

  1. Atualize a classe BlockWidget em main.dart com uma expressão switch que usa padrões de objeto para cada caso:

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),
          ],
        ),
      },
    );
  }
}

Na sua primeira versão do BlockWidget, você alternou em um campo de um objeto Block para retornar um TextStyle. Agora, você alterna uma instância do próprio objeto Block e corresponde a padrões de objetos que representam suas subclasses, extraindo as propriedades do objeto no processo.

O analisador Dart pode verificar se cada subclasse é tratada na expressão switch, porque você transformou Block em uma classe selada.

Observe também que o uso de uma expressão switch aqui permite que você transmita o resultado diretamente para o elemento child, em vez da instrução de retorno separada que era necessária anteriormente.

  1. Execute a recarga automática para fazer a verificação dos dados JSON na caixa de seleção renderizados pela primeira vez:

Uma captura de tela do app que mostra a caixa de seleção "Aprenda Dart 3"

13. Parabéns

Você experimentou com sucesso padrões, registros, switch e casos aprimorados e classes seladas. Você cobriu uma grande quantidade de informações, mas apenas usou superficialmente esses recursos. Para mais informações sobre padrões, consulte a especificação de recursos.

Os diferentes tipos de padrões, os diferentes contextos em que podem aparecer e o possível aninhamento de subpadrões tornam as possibilidades de comportamento aparentemente infinitas. Mas são fáceis de observar.

Você pode imaginar todos os tipos de maneiras de mostrar conteúdo no Flutter usando padrões. Assim, você pode extrair dados com segurança para criar sua interface em poucas linhas de código.

Qual é a próxima etapa?

  • Confira a documentação sobre padrões, registros, switch e casos aprimorados e modificadores de classe na seção Linguagem da documentação do Dart.

Documentos de referência

Confira o exemplo completo no próprio repositório.

Para especificações detalhadas de cada novo recurso, consulte os documentos de design originais: