graph TD
T["Tipos em Dart"] --> P["Primitivos"]
T --> C["Coleções"]
T --> O["Objetos Complexos"]
P --> INT["int<br/>(inteiros 64-bit)"]
P --> DBL["double<br/>(ponto flutuante)"]
P --> STR["String<br/>(texto Unicode)"]
P --> BOL["bool<br/>(true / false)"]
C --> LST["List<br/>(lista ordenada)"]
C --> SET["Set<br/>(conjunto sem duplicatas)"]
C --> MAP["Map<br/>(chave → valor)"]
O --> CLS["Suas próprias classes<br/>(Produto, Pedido, Usuario...)"]
style T fill:#cfe2ff,stroke:#0d6efd
style P fill:#d4edda,stroke:#28a745
style C fill:#fff3cd,stroke:#ffc107
style O fill:#f5c6cb,stroke:#dc3545
Módulo 02 — Fundamentos da Linguagem Dart
Este módulo é, provavelmente, o mais denso do semestre — e também um dos mais recompensadores. Você vai estudar Dart com profundidade suficiente para escrever código real no seu Projeto Integrador. Não se trata de um tour superficial pela linguagem: trata-se de construir uma base sólida que vai sustentar tudo o que você aprenderá nos módulos seguintes. Reserve tempo para este material, experimente os exemplos no seu computador e, principalmente, não tenha medo de testar coisas diferentes. A linguagem Dart é um ótimo lugar para explorar.
Por Que Estudar Dart com Profundidade
No Módulo 01, você teve o primeiro contato com o Flutter e com a sintaxe básica do Dart. Você viu classes simples, construtores e interpolação de strings. Mas para escrever o código do seu Projeto Integrador com qualidade — código que seus colegas de equipe consigam ler, manter e evoluir —, você precisa ir muito além disso.
O Dart é uma linguagem que parece simples à primeira vista e revela uma profundidade considerável à medida que você avança. Ela tem um sistema de tipos robusto com null safety, suporte a programação funcional com funções de primeira classe e closures, um modelo de concorrência baseado em eventos que torna a programação assíncrona elegante, e recursos modernos como generics e mixins que permitem escrever código reutilizável sem duplicação. Dominar esses recursos não é um exercício acadêmico: é o que vai permitir que você implemente os modelos de domínio do seu projeto, escreva os providers de estado e consuma a API do backend com o código limpo e correto.
Existe uma ordem lógica para aprender uma linguagem de programação. Você começa pelo mais fundamental — como os dados são representados e manipulados — e vai progredindo até os conceitos que pressupõem os anteriores. Este módulo segue essa ordem. Começaremos com variáveis e tipos, avançaremos para estruturas de controle e funções, depois para orientação a objetos, coleções e, finalmente, programação assíncrona. Cada seção constrói sobre a anterior.
Variáveis, Tipos e o Sistema de Null Safety
A base de tudo: como o Dart representa e protege os dados
Toda linguagem de programação precisa de uma forma de armazenar dados na memória. Em Dart, você faz isso com variáveis. Mas o Dart moderno vai além: ele exige que você declare, desde o início, se uma variável pode ou não conter o valor nulo (null). Essa decisão de projeto, chamada de null safety, é uma das características mais importantes da linguagem.
Declarando Variáveis
Em Dart, você tem quatro formas principais de declarar variáveis, e escolher a forma correta é o primeiro hábito que você precisa desenvolver.
A primeira forma usa a palavra-chave var. Quando você usa var, o compilador Dart infere o tipo da variável a partir do valor que você atribui a ela. Se você escreve var nome = 'Carlos', o compilador sabe que nome é do tipo String e não vai deixar você atribuir um número a ela depois.
A segunda forma declara o tipo explicitamente: String nome = 'Carlos'. Isso é mais verboso, mas deixa a intenção do código mais clara — especialmente quando o tipo não é óbvio a partir do valor.
A terceira forma usa final. Uma variável final pode ser atribuída apenas uma vez. Após a primeira atribuição, seu valor não pode ser alterado. Isso é especialmente útil para objetos que são criados e depois não mudam — como os modelos de domínio do Projeto Integrador.
A quarta forma usa const. Uma variável const é resolvida em tempo de compilação, o que significa que seu valor precisa ser conhecido antes de o programa executar. No Flutter, const tem um papel especial: widgets declarados como const são criados uma única vez e reutilizados pelo framework, o que melhora significativamente o desempenho.
Uma boa regra prática: prefira sempre final para variáveis que não vão mudar após a criação. Use const para valores que você conhece em tempo de compilação (textos, números, listas fixas). Reserve var para casos onde a reatribuição é necessária e o tipo é evidente pelo contexto.
Os Tipos Primitivos do Dart
O Dart tem um conjunto de tipos primitivos que você vai usar constantemente. Diferentemente de linguagens como Java, onde int e Integer são coisas diferentes, em Dart todos os tipos são objetos — inclusive os números. Isso significa que int tem métodos, e você pode chamar .toString() ou .abs() diretamente sobre ele.
O tipo int representa números inteiros sem parte decimal. Em Dart, o tamanho de um int varia dependendo da plataforma: em plataformas nativas (Android, iOS), é um inteiro de 64 bits com sinal, capaz de armazenar valores entre -2^{63} e 2^{63} - 1. O tipo double representa números com ponto flutuante de dupla precisão, seguindo o padrão IEEE 754. O tipo String representa sequências de caracteres Unicode, e o Dart suporta interpolação de strings com a sintaxe $variavel ou ${expressao}. O tipo bool representa valores lógicos: apenas true ou false. Não existe conversão implícita de outros tipos para bool em Dart — uma instrução como if (0) simplesmente não compila.
O Null Safety em Detalhes
O null safety é a característica que mais diferencia o Dart moderno de linguagens mais antigas. A ideia central é simples: por padrão, uma variável não pode ser nula. Se você declara String nome, você está prometendo ao compilador que nome nunca vai ser null. O compilador vai cobrar essa promessa: qualquer código que tente atribuir null a nome não vai compilar.
Para indicar que uma variável pode ser nula, você adiciona ? ao tipo: String? nome. Agora nome pode ser null, e o compilador vai exigir que você trate essa possibilidade em todo lugar que usar nome. Essa exigência parece trabalhosa no início, mas ela elimina uma família inteira de bugs — os famosos erros de “null pointer exception” que derrubam aplicativos em produção — transformando-os em erros de compilação, que você descobre enquanto escreve o código.
O Dart oferece quatro operadores para trabalhar com valores potencialmente nulos de forma segura. O operador ?. (acesso seguro a membros) executa a operação somente se o objeto não for nulo; caso contrário, retorna null. O operador ?? (null-coalescing) retorna o valor da esquerda se ele não for nulo; caso contrário, retorna o valor da direita. O operador ??= atribui um valor a uma variável apenas se ela for atualmente nula. O operador ! (null-assertion) diz ao compilador “eu sei que este valor não é nulo”; se você estiver errado, o programa vai lançar uma exceção em runtime — use este operador apenas quando você tiver certeza absoluta.
02_nullsafety.dart
// -----------------------------------------------------------------------
// Demonstração completa do sistema de null safety do Dart.
// Contexto: modelos do Projeto Integrador (pedidos de um aplicativo).
// -----------------------------------------------------------------------
// Classe que representa um item dentro de um pedido.
// 'observacao' é opcional — o usuário pode ou não preencher.
class ItemPedido {
final String nomeProduto;
final int quantidade;
final double precoUnitario;
final String? observacao; // Pode ser null: campo opcional no formulário
const ItemPedido({
required this.nomeProduto,
required this.quantidade,
required this.precoUnitario,
this.observacao, // Não é 'required': fica null se não fornecido
});
}
void demonstrarNullSafety() {
// Criando um item SEM observação
const item = ItemPedido(
nomeProduto: 'Café Especial',
quantidade: 2,
precoUnitario: 18.90,
// 'observacao' não foi fornecida — será null automaticamente
);
// ---- Operador ?. (acesso seguro a membros) ----
// Se 'observacao' for null, a expressão inteira retorna null
// em vez de lançar uma exceção.
int? tamanhoObs = item.observacao?.length;
// tamanhoObs é null porque 'observacao' é null
// ---- Operador ?? (null-coalescing) ----
// Se 'observacao' for null, usa o texto padrão.
String textoObs = item.observacao ?? 'Sem observação';
print('Observação: $textoObs'); // Imprime: "Sem observação"
// ---- Verificação explícita com if ----
// O compilador "entende" que, dentro do bloco if,
// 'observacao' não pode mais ser null.
if (item.observacao != null) {
// Aqui, usar 'item.observacao' é seguro sem qualquer operador especial.
print('Observação fornecida: ${item.observacao}');
}
// ---- Operador ! (null-assertion) ----
// Use APENAS quando você tem certeza absoluta.
// Se errar, o programa vai encerrar com um erro em runtime.
// Exemplo: após uma verificação sua que o compilador não "vê"
final lista = ['café', 'chá'];
final primeiro = lista.isNotEmpty ? lista.first : null;
// Aqui sei que 'primeiro' não é null porque verifiquei a lista
print('Primeiro item: ${primeiro!}'); // Seguro neste contexto
}Operadores da Linguagem
O Dart possui um conjunto rico de operadores. Você já conhece os aritméticos básicos (+, -, *, /), mas há alguns que são particulares ao Dart e vale destacar.
O operador ~/ realiza divisão inteira — retorna o quociente inteiro de uma divisão, descartando a parte decimal. Por exemplo, 7 ~/ 2 retorna 3. O operador % é o módulo, retornando o resto da divisão inteira: 7 % 2 retorna 1. Esses dois operadores são úteis para cálculos de paginação, por exemplo — determinar quantas páginas são necessárias para exibir um número de itens.
Os operadores de comparação (==, !=, <, >, <=, >=) funcionam como esperado, mas há uma nuance importante: em Dart, == compara por valor para tipos primitivos e para qualquer classe que sobrescreva o operador ==. Você aprenderá a sobrescrever == nas suas classes de domínio mais adiante neste módulo.
O Dart também suporta o operador de cascade (..), que permite encadear múltiplas operações sobre o mesmo objeto sem repetir a referência. É amplamente usado com builders e configurações.
Os operadores lógicos && (e lógico), || (ou lógico) e ! (negação) funcionam com avaliação de curto-circuito: em a && b, se a for false, b não é avaliado; em a || b, se a for true, b não é avaliado. Isso é relevante quando a avaliação de b tem efeitos colaterais.
Estruturas de Controle de Fluxo
As estruturas de controle de fluxo em Dart seguem o padrão de linguagens da família C, o que as torna imediatamente reconhecíveis. O if / else funciona exatamente como em Java ou C#. O for clássico, o for-in para iteração em coleções e o while e do-while também seguem o mesmo padrão. Mas há dois aspectos que merecem atenção especial: o switch com exhaustiveness e o papel do break e continue em laços aninhados.
O if e else
O if em Dart exige uma condição booleana. Não existe coerção de outros tipos para bool — isso é deliberado. Em JavaScript, if (0) é válido e resolve como false. Em Dart, isso simplesmente não compila: você precisa ser explícito. Escreva if (quantidade == 0) ou if (lista.isEmpty).
O Dart tem uma forma condensada do if-else chamada de conditional expression (operador ternário): condicao ? expressaoSeVerdadeiro : expressaoSeFalso. Esse operador é útil para expressões curtas, mas deve ser evitado quando a lógica for complexa — nesses casos, o if-else explícito é sempre mais legível.
O switch em Dart 3
O switch do Dart recebeu uma atualização significativa no Dart 3, tornando-se muito mais poderoso. Na forma clássica, ele funciona como o switch do Java. Mas na forma moderna, ele pode ser usado como uma expressão (retorna um valor diretamente), suporta pattern matching (correspondência de padrões) e tem exhaustiveness checking para enums — o compilador avisa se você esqueceu de tratar algum caso de um enum.
Para o seu Projeto Integrador, o switch moderno é especialmente útil para tratar os diferentes estados de um pedido: se o status for pendente, faça X; se for confirmado, faça Y; se for cancelado, faça Z. O compilador vai garantir que você não esqueceu nenhum status.
O for e suas Variações
O for clássico (for (int i = 0; i < n; i++)) é o de sempre. Mas para iterar sobre coleções, o for-in é mais conciso e legível: for (final item in listaDeProdutos). Quando você também precisa do índice, use o método .asMap().entries da lista, ou simplesmente o for clássico.
As palavras-chave break e continue funcionam normalmente. Em laços aninhados, você pode usar labels para quebrar ou continuar um laço externo a partir de um laço interno — um recurso raramente necessário, mas que existe caso você precise.
02_estruturas_controle.dart
// -----------------------------------------------------------------------
// Estruturas de controle em contexto do Projeto Integrador.
// Cenário: processar a lista de itens de um pedido e calcular totais.
// -----------------------------------------------------------------------
import '02_nullsafety.dart';
// Enum representando os possíveis status de um pedido
enum StatusPedido { pendente, confirmado, preparando, entregue, cancelado }
class Pedido {
final String id;
final List<ItemPedido> itens;
final StatusPedido status;
const Pedido({required this.id, required this.itens, required this.status});
}
// Usando switch como expressão para converter o status em texto legível.
// O Dart verifica se TODOS os casos do enum foram tratados (exhaustiveness).
String statusParaTexto(StatusPedido status) => switch (status) {
StatusPedido.pendente => 'Aguardando confirmação',
StatusPedido.confirmado => 'Pedido confirmado',
StatusPedido.preparando => 'Em preparo',
StatusPedido.entregue => 'Entregue',
StatusPedido.cancelado => 'Cancelado',
};
// Calculando o total de um pedido com for-in.
// 'total' acumula a soma de (preço × quantidade) de cada item.
double calcularTotal(Pedido pedido) {
double total = 0.0;
for (final item in pedido.itens) {
// Cada item multiplica seu preço unitário pela quantidade
total += item.precoUnitario * item.quantidade;
}
return total;
}
// Filtrando itens com observação usando for-in e if.
List<ItemPedido> itensComObservacao(Pedido pedido) {
final resultado = <ItemPedido>[];
for (final item in pedido.itens) {
// Só adiciona ao resultado se tiver observação preenchida
if (item.observacao != null && item.observacao!.isNotEmpty) {
resultado.add(item);
}
}
return resultado;
}
void main() {
final pedido = Pedido(
id: 'PED-001',
itens: [
const ItemPedido(nomeProduto: 'Café Especial', quantidade: 2, precoUnitario: 18.90, observacao: 'Sem açúcar'),
const ItemPedido(
nomeProduto: 'Pão de Queijo',
quantidade: 4,
precoUnitario: 4.50,
// Sem observação
),
],
status: StatusPedido.confirmado,
);
// Switch como expressão
print(statusParaTexto(pedido.status)); // "Pedido confirmado"
// Calculando o total
final total = calcularTotal(pedido);
print('Total: R\$ ${total.toStringAsFixed(2)}'); // "R$ 55.80"
// Verificando quais itens têm observação
final comObs = itensComObservacao(pedido);
print('${comObs.length} item(ns) com observação');
}Funções: A Unidade de Trabalho do Dart
Funções são objetos de primeira classe em Dart
Em Dart, funções são objetos. Isso significa que uma função pode ser atribuída a uma variável, passada como argumento para outra função ou retornada como resultado de outra função. Esse é o alicerce da programação funcional, e o Flutter usa intensivamente esse recurso — os callbacks de botões, os builders de widgets e as transformações de listas são todos exemplos de funções sendo passadas como valores.
Parâmetros Posicionais, Nomeados e Opcionais
O Dart oferece três tipos de parâmetros, e você vai usar todos eles no seu projeto.
Parâmetros posicionais são os tradicionais: a ordem em que você passa os argumentos define a qual parâmetro cada um pertence. Para marcar parâmetros posicionais como opcionais, você os envolve em colchetes: void funcao(String a, [String? b]). O parâmetro b é opcional e pode ser nulo.
Parâmetros nomeados são o estilo preferido no Flutter. Ao chamá-los, você escreve o nome: funcao(nome: 'Carlos', preco: 18.90). Parâmetros nomeados podem ser marcados como required (obrigatórios) ou terem um valor padrão. Em Flutter, praticamente todos os construtores de widgets usam parâmetros nomeados — é o que torna o código tão legível e auto-documentado.
Arrow Functions
Quando uma função contém uma única expressão, você pode usar a sintaxe de seta (=>): double calcularSubtotal(double preco, int qtd) => preco * qtd;. O resultado da expressão é automaticamente retornado. Essa sintaxe não é apenas açúcar sintático; ela comunica claramente que a função é uma transformação pura de entrada em saída, sem efeitos colaterais.
Closures
Uma closure é uma função que “captura” variáveis do escopo em que foi definida. Em Dart, todas as funções são closures — elas têm acesso ao contexto em que foram criadas, mesmo que sejam executadas em outro contexto. No Flutter, você usa closures constantemente nos callbacks dos botões: a função passada ao onPressed captura variáveis do widget, o que permite que ela acesse e modifique o estado local.
Funções como Parâmetros (Callbacks)
Passar funções como parâmetro é um padrão central no Flutter. O tipo de uma função em Dart é declarado como RetornoFunction(TipoParam1, TipoParam2). Por exemplo, void Function(String) é o tipo de uma função que não retorna nada e recebe uma String. O tipo VoidCallback é um alias predefinido no Flutter para void Function().
02_parametros_callbacks.dart
// -----------------------------------------------------------------------
// Funções em Dart: parâmetros nomeados, arrow functions e callbacks.
// Contexto: utilitários de formatação e filtragem para o Projeto Integrador.
// -----------------------------------------------------------------------
import '02_nullsafety.dart';
import '02_estruturas_controle.dart';
// Função com parâmetros nomeados e valor padrão.
// Formata um preço como moeda brasileira.
String formatarMoeda({
required double valor,
String simbolo = 'R\$', // Valor padrão: Real brasileiro
int casasDecimais = 2, // Valor padrão: duas casas decimais
}) {
// toStringAsFixed garante o número certo de casas decimais
return '$simbolo ${valor.toStringAsFixed(casasDecimais)}';
}
// Arrow function: clara e concisa para transformações simples
double calcularDesconto(double preco, double percentual) => preco * (1 - percentual / 100);
// Função que recebe outra função como parâmetro (callback).
// 'onItemProcessado' é chamada para cada item processado com sucesso.
void processarPedido(
Pedido pedido, {
required void Function(ItemPedido item, double subtotal) onItemProcessado,
void Function(String mensagem)? onErro, // Callback opcional para erros
}) {
for (final item in pedido.itens) {
// Validação básica
if (item.quantidade <= 0 || item.precoUnitario <= 0) {
// Se o callback de erro foi fornecido, chama ele
onErro?.call('Item inválido: ${item.nomeProduto}');
continue; // Pula este item e vai para o próximo
}
final subtotal = item.precoUnitario * item.quantidade;
// Chama o callback de sucesso com o item e o subtotal calculado
onItemProcessado(item, subtotal);
}
}
// Closure: a função interna captura 'fatorDesconto' do escopo externo.
// Retorna uma função que aplica um desconto fixo a qualquer preço.
double Function(double) criarAplicadorDeDesconto(double fatorDesconto) {
// Esta função interna "lembra" o valor de 'fatorDesconto'
// mesmo depois que 'criarAplicadorDeDesconto' terminar de executar.
return (double preco) => preco * (1 - fatorDesconto);
}
void main() {
// Usando a função com parâmetros nomeados
print(formatarMoeda(valor: 18.90));
// Imprime: "R$ 18.90"
print(formatarMoeda(valor: 18.90, simbolo: 'USD', casasDecimais: 4));
// Imprime: "USD 18.9000"
// Usando a closure para criar um aplicador de desconto de 10%
final aplicar10PorCento = criarAplicadorDeDesconto(0.10);
print(formatarMoeda(valor: aplicar10PorCento(50.00)));
// Imprime: "R$ 45.00"
// Criando um pedido para testar
final pedido = Pedido(
id: 'PED-002',
itens: [const ItemPedido(nomeProduto: 'Espresso Duplo', quantidade: 1, precoUnitario: 9.00)],
status: StatusPedido.pendente,
);
// Processando o pedido com um callback lambda (closure inline)
processarPedido(
pedido,
onItemProcessado: (item, subtotal) {
// Esta função captura 'formatarMoeda' do escopo externo
print('${item.nomeProduto}: ${formatarMoeda(valor: subtotal)}');
},
onErro: (msg) => print('Erro: $msg'),
);
}Orientação a Objetos em Dart
O Dart é uma linguagem orientada a objetos “de verdade”: todo valor é um objeto, inclusive números e funções. O sistema de orientação a objetos do Dart é ao mesmo tempo familiar para quem conhece Java e mais moderno em aspectos importantes.
Classes e Construtores
Uma classe em Dart define a estrutura de um tipo. Ela tem campos (atributos), métodos e construtores. Aqui começa uma das características mais usadas no Flutter: construtores nomeados. Em Dart, você pode definir múltiplos construtores para a mesma classe, cada um com um nome diferente. Isso é especialmente útil para criar objetos a partir de formatos diferentes de dado — como criar um modelo de domínio a partir de um JSON retornado pela API.
O construtor mais importante que você vai aprender a escrever é o fromJson. Quando o backend da AWS retorna dados, eles chegam como JSON — um mapa de chave-valor. O construtor fromJson converte esse mapa nas suas classes de domínio. O método toJson, por simetria, converte o objeto de volta para um mapa quando você precisa enviar dados ao backend.
Imutabilidade e o Método copyWith
Uma prática muito adotada no Flutter é tornar os modelos de domínio imutáveis: todos os campos são final, e os objetos não mudam depois de criados. Quando você precisa de uma versão “modificada” de um objeto, cria um novo objeto com os campos alterados — e o método copyWith é o padrão para fazer isso de forma legível.
O copyWith recebe todos os campos como parâmetros opcionais (usando null para indicar “mantenha o valor atual”) e retorna um novo objeto com os valores especificados substituídos. Esse padrão é central no gerenciamento de estado do Flutter: quando o estado de um Provider precisa mudar, você cria um novo objeto de estado em vez de modificar o existente.
Sobrescrevendo == e hashCode
Para que duas instâncias da sua classe de domínio sejam consideradas iguais quando têm os mesmos dados, você precisa sobrescrever o operador == e o hashCode. Em Dart, por padrão, == compara por identidade (duas variáveis são iguais apenas se apontam para o mesmo objeto na memória). Ao sobrescrever, você define o que significa “igualdade de valor” para a sua classe.
O hashCode precisa ser consistente com ==: se dois objetos são iguais (segundo seu ==), eles devem ter o mesmo hashCode. Em Dart, a forma mais simples é usar Object.hash(), que combina os hash codes dos campos relevantes.
02_fromtojson_copywith.dart
// -----------------------------------------------------------------------
// Classes de domínio do Projeto Integrador com boas práticas Dart:
// imutabilidade, fromJson/toJson, copyWith, == e hashCode.
// -----------------------------------------------------------------------
class Produto {
// Campos finais: o objeto é imutável após a criação
final String id;
final String nome;
final double preco;
final String? descricao;
final String categoria;
final bool disponivel;
// Construtor principal: usa parâmetros nomeados para clareza.
// 'const' permite criar instâncias em tempo de compilação.
const Produto({
required this.id,
required this.nome,
required this.preco,
required this.categoria,
this.descricao,
this.disponivel = true, // Valor padrão: produto disponível
});
// Construtor nomeado 'fromJson': cria um Produto a partir do JSON da API.
// 'json' é um Map<String, dynamic> — o formato que o pacote 'http' retorna.
factory Produto.fromJson(Map<String, dynamic> json) {
return Produto(
// O operador 'as' faz o cast do tipo dinâmico para o tipo esperado.
// Se o tipo for incompatível, lança uma exceção em runtime.
id: json['id'] as String,
nome: json['nome'] as String,
preco: (json['preco'] as num).toDouble(), // num → double
categoria: json['categoria'] as String,
// Para campos opcionais, verificamos se a chave existe E não é null
descricao: json['descricao'] as String?,
// JSON pode retornar bool ou 1/0; garantimos bool aqui
disponivel: (json['disponivel'] as bool?) ?? true,
);
}
// Método 'toJson': converte o Produto de volta para Map (para enviar à API).
Map<String, dynamic> toJson() {
return {
'id': id,
'nome': nome,
'preco': preco,
'categoria': categoria,
// Campos nullable são incluídos somente se tiverem valor
if (descricao != null) 'descricao': descricao,
'disponivel': disponivel,
};
}
// Método 'copyWith': retorna um NOVO Produto com alguns campos alterados.
// Todos os parâmetros são opcionais; campos não fornecidos mantêm o valor atual.
// Convenção: use Object? com valor padrão de Object() para distinguir
// "não fornecido" de "fornecido como null".
Produto copyWith({
String? id,
String? nome,
double? preco,
String? categoria,
Object? descricao = const Object(), // Sentinela para campo nullable
bool? disponivel,
}) {
return Produto(
id: id ?? this.id,
nome: nome ?? this.nome,
preco: preco ?? this.preco,
categoria: categoria ?? this.categoria,
// Se 'descricao' foi passado como argumento (mesmo que null),
// usa o novo valor; caso contrário, mantém o atual.
descricao: descricao == const Object() ? this.descricao : descricao as String?,
disponivel: disponivel ?? this.disponivel,
);
}
// Sobrescrita de '==' para comparação por valor.
// Dois Produtos são iguais se têm o mesmo 'id'.
@override
bool operator ==(Object other) =>
identical(this, other) || // Otimização: mesmo objeto na memória
other is Produto && runtimeType == other.runtimeType && id == other.id;
// hashCode deve ser consistente com '=='.
// Como '==' compara apenas por 'id', hashCode também usa apenas 'id'.
@override
int get hashCode => id.hashCode;
// toString para facilitar o debug
@override
String toString() => 'Produto(id: $id, nome: $nome, preco: $preco, disponivel: $disponivel)';
}
void main() {
// Criando um produto diretamente
const produto = Produto(id: 'prod-001', nome: 'Cappuccino', preco: 14.50, categoria: 'Bebidas', descricao: 'Espresso com leite vaporizado');
// Simulando o que chegaria como JSON da API
final jsonDaApi = {'id': 'prod-001', 'nome': 'Cappuccino', 'preco': 14.50, 'categoria': 'Bebidas', 'disponivel': true};
final produtoDaApi = Produto.fromJson(jsonDaApi);
// Os dois produtos têm o mesmo 'id', então são iguais segundo nosso '=='
print(produto == produtoDaApi); // true
// Criando uma versão do produto com preço atualizado (sem modificar o original)
final produtoAtualizado = produto.copyWith(preco: 15.90);
print(produtoAtualizado.preco); // 15.90
print(produto.preco); // 14.50 — o original não mudou
}Herança e Polimorfismo
O Dart usa extends para herança e @override para sobrescrever métodos. A herança em Dart é simples: uma classe pode ter apenas um pai direto. Isso é diferente de algumas linguagens que suportam herança múltipla. Em troca, o Dart oferece mixins — uma forma elegante de reutilizar comportamento entre classes sem criar uma hierarquia de herança.
Um mixin é definido com a palavra-chave mixin e aplicado com with. Você pode aplicar múltiplos mixins a uma classe. A diferença entre um mixin e uma interface é que o mixin pode conter implementação de métodos, não apenas declaração.
No contexto do seu Projeto Integrador, mixins são úteis para comportamentos transversais: validação, serialização, ou auditoria (registrar quando um objeto foi criado ou modificado).
Classes Abstratas e Interfaces
Em Dart, não existe a palavra-chave interface. Em vez disso, toda classe pode ser usada como interface. Quando você quer que uma classe sirva apenas como contrato (sem implementação), usa abstract class. Quando quer apenas declarar métodos abstratos sem nenhuma implementação, pode também usar abstract interface class (Dart 3).
No contexto da Arquitetura Hexagonal que você estuda no Projeto Integrador, classes abstratas definem os repositórios — as interfaces do domínio que o Flutter vai usar sem saber se os dados vêm do SQLite local ou da API da AWS.
Generics: Tipos que Funcionam para Qualquer Tipo
Generics são um mecanismo para escrever código que funciona com diferentes tipos sem duplicação. Uma List<Produto> é uma lista específica de produtos; uma List<String> é uma lista de textos. O tipo entre <> é o parâmetro de tipo — ele especializa o comportamento genérico para um tipo concreto.
O Dart usa generics extensivamente. Todas as coleções (List, Set, Map) são genéricas. A classe Future<T> representa um resultado assíncrono que eventualmente produzirá um valor do tipo T. Você vai criar suas próprias classes genéricas quando implementar o padrão Result<T> — um tipo que representa ou um sucesso com valor do tipo T ou uma falha com uma mensagem de erro.
O padrão Result<T> é muito usado no Flutter para representar o resultado de operações que podem falhar (como uma chamada à API). Em vez de lançar exceções — o que dificulta o controle de fluxo —, você retorna um Result<T> e quem chamou decide o que fazer nos casos de sucesso e de erro.
// -----------------------------------------------------------------------
// Generics: o padrão Result<T> para tratamento seguro de erros.
// Contexto: chamadas à API do Projeto Integrador que podem falhar.
// -----------------------------------------------------------------------
// Classe base abstrata: ou é um sucesso ou é uma falha
sealed class Result<T> {
const Result();
}
// Caso de sucesso: contém o valor produzido
class Success<T> extends Result<T> {
final T value;
const Success(this.value);
}
// Caso de falha: contém a mensagem de erro
class Failure<T> extends Result<T> {
final String mensagem;
final Object? erro; // A exceção original, se disponível
const Failure(this.mensagem, {this.erro});
}
// Repositório abstrato de produtos — contrato do domínio.
// A implementação concreta (que sabe se vai ao SQLite ou à API)
// é injetada via GetIt, sem que o código de negócios saiba como.
abstract class ProdutoRepository {
Future<Result<List<Produto>>> buscarTodos();
Future<Result<Produto>> buscarPorId(String id);
Future<Result<void>> salvar(Produto produto);
}
// Usando o padrão Result em um caso de uso (use case).
// Esta função não sabe de onde vêm os produtos — só sabe o contrato.
Future<void> carregarProdutos(ProdutoRepository repositorio) async {
// Chamamos o repositório e recebemos um Result
final resultado = await repositorio.buscarTodos();
// O switch com 'sealed class' garante que tratamos todos os casos
switch (resultado) {
case Success<List<Produto>>(:final value):
// Se teve sucesso, 'value' já é List<Produto> — sem cast necessário
print('${value.length} produtos carregados');
for (final produto in value) {
print(' - ${produto.nome}: R\$ ${produto.preco.toStringAsFixed(2)}');
}
case Failure<List<Produto>>(:final mensagem, :final erro):
// Se falhou, tratamos o erro sem deixar ele vazar para a interface
print('Erro ao carregar produtos: $mensagem');
if (erro != null) print('Detalhe técnico: $erro');
}
}Coleções: List, Set e Map
As estruturas de dados fundamentais do Dart
Todo aplicativo manipula coleções de dados. Uma lista de produtos, um conjunto de categorias únicas, um mapa de configurações — todas essas estruturas têm representações nativas em Dart que você vai usar diariamente no seu Projeto Integrador. Mais do que saber declarar uma lista, você precisa saber transformá-la, filtrá-la e reduzi-la com as ferramentas funcionais que o Dart disponibiliza.
List: Sequências Ordenadas
A List<T> é a estrutura de dados mais usada no Flutter. Ela representa uma sequência ordenada de elementos, onde cada elemento tem um índice numérico começando em zero. As listas em Dart podem ser mutáveis ou imutáveis. Uma lista literal padrão ([a, b, c]) é mutável. Uma lista criada com List.unmodifiable(outraLista) ou const [a, b, c] é imutável — qualquer tentativa de modificá-la lança uma exceção.
Os métodos mais importantes da List são os de transformação funcional, herdados da classe Iterable. O método map((e) => ...) recebe uma função e retorna um novo Iterable onde cada elemento foi transformado pela função. O método where((e) => ...) filtra os elementos, retornando apenas aqueles para os quais a condição é verdadeira. O método reduce((acc, e) => ...) combina todos os elementos em um único valor, acumulando o resultado. O método fold(valorInicial, (acc, e) => ...) é similar ao reduce, mas permite especificar um valor inicial — o que evita o erro que reduce lança em listas vazias.
Esses métodos retornam Iterable, não List. Para obter uma List, chame .toList() no final da cadeia. Para obter um Set, chame .toSet().
Set: Conjuntos sem Duplicatas
O Set<T> é uma coleção que não admite elementos duplicados. Ele é útil quando você precisa de um conjunto de categorias únicas, de IDs únicos, ou quando quer verificar rapidamente se um elemento está na coleção (a operação contains em um Set é O(1) — tempo constante — enquanto em uma List é O(n)).
Map: Associações Chave-Valor
O Map<K, V> associa chaves do tipo K a valores do tipo V. Em Dart, o JSON desserializado chega como Map<String, dynamic> — a chave é sempre uma String (o nome do campo JSON), e o valor é dynamic (qualquer tipo). Essa é exatamente a estrutura que você vai receber da API da AWS e converter nos seus modelos de domínio usando fromJson.
Um padrão frequente no Flutter é usar Map<String, Produto> (ou outro modelo de domínio) como estrutura de cache em memória: a chave é o ID do produto, e o valor é o próprio produto. Isso permite busca por ID em tempo constante O(1), sem precisar iterar por toda a lista a cada busca.
A Programação Funcional sobre Coleções
Encadear transformações sobre coleções é um dos aspectos mais elegantes do Dart moderno. Em vez de loops imperativos com variáveis auxiliares, você pode expressar transformações complexas como uma cadeia de chamadas de método, cada uma descrevendo o que fazer, não como fazer.
// -----------------------------------------------------------------------
// Coleções e programação funcional.
// Contexto: processamento do catálogo de produtos do Projeto Integrador.
// -----------------------------------------------------------------------
void demonstrarColecoes(List<Produto> catalogo) {
// ---- List: transformações funcionais ----
// map: transforma cada Produto em uma String com nome e preço
final nomes = catalogo
.map((p) => '${p.nome} (R\$ ${p.preco.toStringAsFixed(2)})')
.toList();
// nomes é uma List<String>
// where: filtra apenas produtos disponíveis
final disponiveis = catalogo
.where((p) => p.disponivel)
.toList();
// disponiveis é uma List<Produto> apenas com os disponíveis
// where + map encadeados: nomes dos produtos disponíveis
final nomesDisponiveis = catalogo
.where((p) => p.disponivel)
.map((p) => p.nome)
.toList();
// fold: calcula o preço médio (evita divisão por zero)
final totalPrecos = catalogo.fold<double>(
0.0,
(acumulador, produto) => acumulador + produto.preco,
);
final precoMedio = catalogo.isEmpty ? 0.0 : totalPrecos / catalogo.length;
print('Preço médio: R\$ ${precoMedio.toStringAsFixed(2)}');
// any: retorna true se ALGUM produto custar mais de R$ 50
final temProdutoCaro = catalogo.any((p) => p.preco > 50.0);
// every: retorna true se TODOS os produtos estiverem disponíveis
final todosDisponiveis = catalogo.every((p) => p.disponivel);
// sort: ordena por preço (atenção: sort modifica a lista original!)
final catalogoOrdenado = [...catalogo] // Cria uma cópia antes de ordenar
..sort((a, b) => a.preco.compareTo(b.preco));
// ---- Set: categorias únicas do catálogo ----
// O Set automaticamente remove duplicatas
final categoriasUnicas = catalogo
.map((p) => p.categoria)
.toSet(); // Set<String> com categorias sem repetição
print('Categorias: ${categoriasUnicas.join(', ')}');
// ---- Map: índice de produtos por ID ----
// Cria um mapa id → produto para busca eficiente
final indicePorId = {
for (final produto in catalogo) produto.id: produto
};
// Buscar um produto por ID agora é O(1) em vez de O(n)
final produtoBuscado = indicePorId['prod-001'];
if (produtoBuscado != null) {
print('Encontrado: ${produtoBuscado.nome}');
}
// ---- Agrupando produtos por categoria ----
// Resultado: Map<String, List<Produto>>
final porCategoria = <String, List<Produto>>{};
for (final produto in catalogo) {
// getOrPut: cria a lista se a chave não existir
porCategoria.putIfAbsent(produto.categoria, () => []).add(produto);
}
// Equivalente funcional com groupBy (disponível com collection package):
// final porCategoria = catalogo.groupBy((p) => p.categoria);
print('Produtos por categoria:');
for (final entrada in porCategoria.entries) {
print(' ${entrada.key}: ${entrada.value.length} produto(s)');
}
}Programação Assíncrona com Future e async/await
O tema mais importante para o desenvolvimento Flutter
Toda operação que demora tempo — uma requisição HTTP, uma consulta ao banco de dados, a leitura de um arquivo — precisa ser executada de forma assíncrona em um aplicativo mobile. Se você bloquear a thread principal esperando uma resposta da rede, a interface do usuário vai travar. O usuário vai ver um aplicativo que parou de responder, não vai conseguir rolar a tela, e provavelmente vai fechar o app frustrado. Entender programação assíncrona não é opcional: é o alicerce de qualquer aplicativo Flutter funcional.
O Modelo de Execução do Dart
Para entender a programação assíncrona, você precisa entender como o Dart executa código. O Dart usa um event loop (laço de eventos): um único thread que continuamente verifica duas filas — a fila de microtasks (tarefas muito pequenas e de alta prioridade) e a fila de eventos (callbacks de I/O, timers, e eventos de usuário).
Quando você faz uma chamada de rede, o Dart não fica esperando a resposta. Ele registra um callback para quando a resposta chegar e continua executando outro código. Quando a resposta da rede finalmente chega, o callback é colocado na fila de eventos. Na próxima iteração do event loop, o callback é executado.
Essa arquitetura é chamada de single-threaded concurrency (concorrência com thread único). Ela é diferente do modelo multi-thread de Java, onde você cria threads separadas para operações longas. Em Dart, o código de interface e o código de lógica de negócios rodam no mesmo thread — mas as operações de I/O acontecem fora dele, nos níveis mais baixos do sistema operacional.
%%| fig-width: 9
sequenceDiagram
participant App as Aplicativo Flutter
participant EventLoop as Event Loop do Dart
participant SO as Sistema Operacional / Rede
App->>EventLoop: buscarProdutos() — registra callback
EventLoop->>SO: Inicia a requisição HTTP (não bloqueia)
Note over EventLoop: Continua executando outros eventos<br/>(gestos, animações, rebuilds)
SO-->>EventLoop: Resposta HTTP chegou — coloca callback na fila
EventLoop->>App: Executa o callback com os dados recebidos
App->>App: Atualiza o estado — interface redesenhadaFuture: Uma Promessa de Valor Futuro
Um Future<T> representa um valor do tipo T que ainda não está disponível, mas estará em algum momento no futuro. É a promessa de um resultado. Um Future<List<Produto>> representa uma lista de produtos que eventualmente vai chegar — provavelmente da API da AWS.
Um Future pode estar em três estados: pendente (a operação ainda não terminou), concluído com sucesso (o valor está disponível) ou concluído com erro (a operação falhou).
async e await: A Forma Legível de Escrever Assincronismo
A palavra-chave async marca uma função como assíncrona. Uma função async sempre retorna um Future. A palavra-chave await suspende a execução da função async atual até que o Future à sua frente se resolva — mas sem bloquear o event loop. Durante a espera, o Dart continua processando outros eventos.
A sintaxe async/await é “açúcar sintático” sobre os callbacks do Future: ela transforma código assíncrono em código que parece síncrono e sequencial, muito mais fácil de ler e de entender.
Tratamento de Erros com try/catch
Em código assíncrono com async/await, exceções são tratadas com try/catch normalmente. O await “desempacota” o Future, e se o Future concluiu com erro, a exceção é lançada no ponto do await, onde o try/catch pode capturá-la.
Você pode usar on TipoDeExcecao para capturar tipos específicos de exceção antes de um catch genérico. A cláusula finally é executada sempre, independentemente de ter havido erro — útil para liberar recursos.
// -----------------------------------------------------------------------
// Programação assíncrona: Future, async/await e tratamento de erros.
// Contexto: consumir a API da AWS no Projeto Integrador.
// -----------------------------------------------------------------------
import 'dart:convert'; // Para jsonDecode
import 'package:http/http.dart' as http;
// Exceções customizadas para comunicar falhas de forma descritiva
class ApiException implements Exception {
final int statusCode;
final String mensagem;
const ApiException(this.statusCode, this.mensagem);
@override
String toString() => 'ApiException($statusCode): $mensagem';
}
class SemConexaoException implements Exception {
const SemConexaoException();
@override
String toString() => 'SemConexaoException: Sem conexão com a internet';
}
// Serviço responsável por buscar produtos na API da AWS.
// Esta classe não sabe sobre widgets — ela é pura lógica de dados.
class ProdutoApiService {
final String baseUrl;
const ProdutoApiService({required this.baseUrl});
// Método assíncrono: retorna um Future<List<Produto>>.
// O 'async' habilita o uso de 'await' dentro da função.
Future<List<Produto>> buscarProdutos({String? categoria}) async {
// Monta a URL com parâmetro de query opcional
final uri = Uri.parse('$baseUrl/produtos').replace(
queryParameters: categoria != null ? {'categoria': categoria} : null,
);
try {
// 'await' suspende a função até que a resposta HTTP chegue.
// O Dart continua processando outros eventos durante a espera.
final response = await http.get(
uri,
headers: {'Content-Type': 'application/json'},
);
// Verificar o código HTTP de status
if (response.statusCode == 200) {
// jsonDecode transforma o texto JSON em List<dynamic>
final List<dynamic> jsonList = jsonDecode(response.body) as List;
// Converte cada Map<String, dynamic> em um objeto Produto
return jsonList
.map((json) => Produto.fromJson(json as Map<String, dynamic>))
.toList();
} else if (response.statusCode == 404) {
return []; // Nenhum produto encontrado — retorna lista vazia
} else {
// Erro de servidor: lançamos nossa exceção customizada
throw ApiException(
response.statusCode,
'Erro ao buscar produtos: ${response.statusCode}',
);
}
} on http.ClientException catch (e) {
// ClientException: erro de rede (sem internet, DNS falhou, etc.)
// 'on TipoEspecífico' captura apenas esse tipo de exceção
throw SemConexaoException();
}
// Não capturamos outros erros aqui: ApiException e erros inesperados
// propagam para quem chamou este método.
}
// Cria um produto na API (POST)
Future<Produto> criarProduto(Produto produto) async {
final response = await http.post(
Uri.parse('$baseUrl/produtos'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(produto.toJson()),
);
if (response.statusCode == 201) {
// 201 Created: o servidor retorna o produto criado (com ID gerado)
return Produto.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
} else {
throw ApiException(response.statusCode, 'Falha ao criar produto');
}
}
}
// Exemplo de uso: função que busca produtos e trata os possíveis erros.
// Esta função seria chamada em um Provider do Flutter.
Future<void> carregarCatalogo(ProdutoApiService service) async {
try {
print('Buscando produtos...');
final produtos = await service.buscarProdutos(categoria: 'Bebidas');
print('${produtos.length} produtos carregados com sucesso');
} on SemConexaoException {
// Tratar ausência de conexão de forma específica
print('Sem internet. Tente novamente mais tarde.');
} on ApiException catch (e) {
// Tratar erros da API com as informações disponíveis
print('Erro da API: ${e.mensagem} (código ${e.statusCode})');
} catch (e, stackTrace) {
// Tratar qualquer outro erro inesperado
print('Erro inesperado: $e');
print('Stack trace: $stackTrace');
} finally {
// Executado sempre, com ou sem erro
print('Operação de carregamento finalizada');
}
}Future.wait: Executando Futuros em Paralelo
Às vezes você precisa executar várias operações assíncronas ao mesmo tempo e esperar por todas elas. Por exemplo: carregar o catálogo de produtos e o perfil do usuário ao mesmo tempo, em vez de esperar um terminar para começar o outro.
Future.wait<T>(List<Future<T>> futuros) recebe uma lista de Futures, executa todos em paralelo e retorna um único Future<List<T>> que se resolve quando todos os futuros se resolverem. Se qualquer um falhar, o Future.wait falha imediatamente.
Streams: Sequências de Eventos ao Longo do Tempo
Quando o Future não é suficiente
Um Future representa um único valor que chegará no futuro. Mas e quando você precisa representar uma sequência de valores que chegam ao longo do tempo? O saldo de uma conta que atualiza a cada segundo. As mensagens de um chat que chegam enquanto o usuário conversa. A localização GPS sendo atualizada continuamente. Para esses casos, o Dart tem as Streams.
Uma Stream<T> é uma sequência assíncrona de valores do tipo T. Você se “inscreve” em uma Stream (com listen ou await for) e recebe notificações cada vez que um novo valor é emitido. A Stream pode emitir zero, um ou muitos valores ao longo do tempo, e pode ser finalizada (com ou sem erro).
No Flutter, Streams aparecem principalmente em dois contextos. O primeiro é na comunicação em tempo real: quando o servidor envia dados para o cliente sem que o cliente tenha solicitado (usando WebSockets, por exemplo). O segundo, e mais relevante para o seu Projeto Integrador, é no padrão de gerenciamento de estado: quando um StreamController emite um novo estado toda vez que algo muda, e os widgets que escutam a Stream são reconstruídos automaticamente.
O StreamController<T> é o objeto que você usa para criar e controlar uma Stream. Você chama controller.sink.add(valor) para emitir um valor, controller.sink.addError(erro) para emitir um erro, e controller.close() para fechar a Stream.
// -----------------------------------------------------------------------
// Streams: sequências de valores ao longo do tempo.
// Contexto: rastrear o status de um pedido em tempo real.
// -----------------------------------------------------------------------
import 'dart:async'; // Necessário para StreamController
// Simula um serviço que rastreia o status de um pedido
// e emite atualizações conforme o pedido avança.
class RastreamentoPedidoService {
// StreamController do tipo broadcast: permite múltiplos listeners.
// Use StreamController.broadcast() quando mais de um widget precisa
// escutar a mesma Stream.
final _controller = StreamController<StatusPedido>.broadcast();
// Expõe apenas a Stream (não o Controller) — encapsulamento correto
Stream<StatusPedido> get statusStream => _controller.stream;
// Simula o progresso de um pedido (em um app real, isso viria do servidor)
Future<void> simularProgresso(String pedidoId) async {
final etapas = [
StatusPedido.confirmado,
StatusPedido.preparando,
StatusPedido.entregue,
];
for (final status in etapas) {
// Emite o novo status para todos os listeners
_controller.sink.add(status);
// Simula o tempo entre cada mudança de status
await Future.delayed(const Duration(seconds: 2));
}
// Fecha o controller quando o processo termina
await _controller.close();
}
// IMPORTANTE: sempre fechar o StreamController quando não for mais usado
// para evitar vazamentos de memória
void dispose() {
if (!_controller.isClosed) {
_controller.close();
}
}
}
// Consumindo a Stream com await for
Future<void> monitorarPedido(RastreamentoPedidoService service) async {
print('Monitorando pedido...');
// 'await for' é como um 'for-in' para Streams: espera cada novo valor
await for (final status in service.statusStream) {
print('Novo status: ${statusParaTexto(status)}');
}
print('Pedido finalizado — Stream encerrada');
}
// Consumindo a Stream com listen (estilo callback)
void monitorarComListen(RastreamentoPedidoService service) {
// listen retorna uma StreamSubscription que você pode usar para cancelar
final subscription = service.statusStream.listen(
(status) {
// Chamado cada vez que um novo status é emitido
print('Novo status: ${statusParaTexto(status)}');
},
onError: (Object erro) {
// Chamado se a Stream emitir um erro
print('Erro no rastreamento: $erro');
},
onDone: () {
// Chamado quando a Stream é fechada
print('Rastreamento concluído');
},
);
// Exemplo: cancelar o rastreamento após 5 segundos
Future.delayed(const Duration(seconds: 5), () {
subscription.cancel();
print('Rastreamento cancelado pelo usuário');
});
}Extension Methods: Estendendo Tipos Existentes
Os extension methods são um recurso do Dart que permite adicionar métodos a classes existentes sem precisar modificá-las ou criar subclasses. Isso é especialmente útil para adicionar utilitários a tipos primitivos ou a classes de bibliotecas que você não controla.
No Flutter, o pattern de extensão sobre BuildContext é amplamente usado para acessar recursos do contexto (tema, localização, navegação) de forma mais concisa. No seu Projeto Integrador, você pode usar extensões para adicionar formatação a double (formatação de moeda), para adicionar utilitários a String (capitalização, validação) e para adicionar métodos de conveniência a DateTime.
// -----------------------------------------------------------------------
// Extension methods: utilitários específicos do Projeto Integrador.
// -----------------------------------------------------------------------
// Extensão sobre double para formatação de moeda
extension DoubleMoedaExtension on double {
// Formata o double como Real Brasileiro (R$ 18,90)
String get emReais => 'R\$ ${toStringAsFixed(2).replaceAll('.', ',')}';
// Verifica se é um preço válido (positivo e não absurdamente alto)
bool get isPrecioValido => this > 0 && this < 99999.99;
}
// Extensão sobre String para validações comuns
extension StringValidacaoExtension on String {
// Verifica se é um email válido (verificação básica de formato)
bool get isEmailValido =>
RegExp(r'^[\w.-]+@[\w.-]+\.\w{2,}$').hasMatch(this);
// Capitaliza a primeira letra de cada palavra
String get capitalizado => split(' ')
.map((palavra) => palavra.isEmpty
? ''
: '${palavra[0].toUpperCase()}${palavra.substring(1).toLowerCase()}')
.join(' ');
// Remove espaços extras e normaliza para comparação
String get normalizado => trim().toLowerCase();
}
// Extensão sobre List<Produto> para operações específicas do domínio
extension ListaProdutosExtension on List<Produto> {
// Filtra pelo termo de busca (nome ou categoria, case-insensitive)
List<Produto> filtrarPor(String termo) {
final termoBusca = termo.normalizado;
return where((p) =>
p.nome.normalizado.contains(termoBusca) ||
p.categoria.normalizado.contains(termoBusca)
).toList();
}
// Calcula o preço médio da lista
double get precoMedio =>
isEmpty ? 0.0 : fold<double>(0, (s, p) => s + p.preco) / length;
// Retorna a lista ordenada por preço crescente (sem modificar o original)
List<Produto> get ordenadosPorPreco =>
[...this]..sort((a, b) => a.preco.compareTo(b.preco));
}
// Usando as extensões
void demonstrarExtensoes(List<Produto> catalogo) {
// Extensão sobre double
final preco = 18.9;
print(preco.emReais); // "R$ 18,90"
print(preco.isPrecioValido); // true
// Extensão sobre String
final email = 'usuario@exemplo.com';
print(email.isEmailValido); // true
final nome = 'joão da silva';
print(nome.capitalizado); // "João Da Silva"
// Extensão sobre List<Produto>
final encontrados = catalogo.filtrarPor('café');
print('${encontrados.length} produtos encontrados');
print('Preço médio: ${catalogo.precoMedio.emReais}');
final ordenados = catalogo.ordenadosPorPreco;
// 'catalogo' continua com a ordem original
}Tratamento de Exceções: A Estratégia Completa
O tratamento de exceções em Dart segue um padrão que você já viu em Java ou C#, mas com algumas nuances importantes. Entender quando lançar exceções, quando usar o padrão Result<T> e como estruturar a hierarquia de exceções do seu projeto é uma habilidade que distingue um código bom de um código problemático.
A decisão entre lançar uma exceção e retornar um Result<T> segue uma heurística simples. Exceções são para erros inesperados de programação — condições que indicam um bug, como receber um tipo de dado errado ou tentar usar um objeto que foi descartado. O padrão Result<T> é para erros de negócio esperados — condições que podem acontecer normalmente em produção, como a API retornar 404, o usuário não ter conexão, ou um CPF inválido ser digitado.
No seu Projeto Integrador, a camada de infraestrutura (que fala com a API e com o SQLite) lança exceções quando algo técnico falha. A camada de domínio captura essas exceções e as converte em objetos Failure, retornando um Result<T>. A camada de apresentação (widgets e Providers) recebe o Result<T> e decide o que mostrar na tela — sem nunca ver exceções cruas.
Análise Assintótica das Coleções Dart
Para tomar decisões corretas sobre quais estruturas de dados usar, você precisa entender a complexidade das operações principais de cada coleção. Em um aplicativo real com centenas ou milhares de produtos, a diferença entre O(1) e O(n) pode ser a diferença entre um app rápido e um app lento.
A tabela abaixo resume as complexidades das operações mais comuns nas coleções Dart. A notação O(n) significa que o tempo de execução cresce linearmente com o tamanho n da coleção; O(1) significa tempo constante independente do tamanho.
| Operação | List | Set (HashSet) | Map (HashMap) |
|---|---|---|---|
| Acesso por índice/chave | O(1) | N/A | O(1) |
Busca por valor (contains) |
O(n) | O(1) | O(1) para chave |
| Inserção no final | O(1) amortizado | O(1) amortizado | O(1) amortizado |
| Remoção por valor | O(n) | O(1) | O(1) para chave |
| Iteração completa | O(n) | O(n) | O(n) |
| Ordenação | O(n \log n) | N/A | N/A |
A consequência prática dessa tabela para o seu Projeto Integrador: quando você precisa verificar repetidamente se um produto está em uma lista de favoritos, use um Set<String> de IDs (ou um Map<String, Produto>) em vez de uma List<Produto>. A operação contains será O(1) em vez de O(n).
Null Safety: O Que o Compilador Garante
O sistema de tipos como sua rede de segurança
Agora que você viu o null safety em ação ao longo de todo este módulo, é importante ter uma visão formal do que o sistema de tipos do Dart garante. Esta garantia é rigorosa: ela vale em tempo de compilação e não precisa de testes para ser verificada.
Formalmente, o sistema de null safety do Dart divide o universo de tipos em dois conjuntos. O conjunto \mathcal{N} contém todos os tipos não-anuláveis (como String, int, Produto): uma variável desse tipo nunca é null. O conjunto \mathcal{N}^? contém todos os tipos anuláveis (como String?, int?, Produto?): uma variável desse tipo pode ser null.
O compilador Dart faz análise de fluxo para rastrear se uma variável pode ser nula em cada ponto do código. Quando você escreve if (x != null) { ... }, dentro do bloco if o compilador sabe que x não é nulo, e promove automaticamente o tipo de String? para String. Essa promoção automática elimina a necessidade de usar ! (null-assertion) na maioria dos casos.
Evite o uso de ! (null-assertion) em código de produção. Ele é um “acredite em mim, compilador” que pode explodir em runtime. Se você se vê usando ! frequentemente, é um sinal de que o design das suas classes pode ser melhorado — provavelmente algum campo deveria ser obrigatório (required) em vez de opcional.
As Convenções de Código Dart que o Mercado Espera
Escrever código que funciona é o mínimo. Escrever código que outros desenvolvedores (incluindo você mesmo, em seis meses) consigam ler e manter é o que o mercado espera. As convenções do Dart são descritas no guia “Effective Dart” e são aplicadas automaticamente pela ferramenta dart analyze e pelo flutter_lints — ambos já configurados no seu projeto.
Os pontos mais importantes para este módulo: prefira final a var para variáveis que não mudam. Use tipos explícitos nas assinaturas de funções e campos de classe — reserve a inferência de tipos para variáveis locais onde o tipo é óbvio. Documente métodos públicos com comentários em formato /// (doc comments), pois eles aparecem no tooltip do VS Code. Nomeie as funções de forma que descrevam o que elas fazem, não como fazem: calcularTotal() é melhor que somar(), e buscarProdutosPorCategoria() é mais claro que filtrar().
O Que Você Precisa Implementar no Projeto Integrador
Módulo 02 do Projeto Integrador: os modelos de domínio
Ao estudar este módulo, você construiu o vocabulário técnico necessário para implementar os modelos de domínio do seu Projeto Integrador. Agora chegou a hora de colocar isso em prática.
O entregável deste módulo é uma pasta lib/models/ dentro do repositório do seu projeto, contendo as classes que representam os conceitos principais da aplicação que seu grupo está construindo. Cada classe precisa ter: campos com tipos corretos e null safety adequado, um construtor principal com parâmetros nomeados, um construtor fromJson para desserialização da API, um método toJson para serialização, um método copyWith para criar versões modificadas sem alterar o original, e sobrescrita de == e hashCode para comparação por valor.
Além das classes de modelo, você vai criar um arquivo lib/models/enums.dart com os enums do domínio — os possíveis status de um pedido, os tipos de categoria, ou qualquer outro conjunto fixo de valores que apareça no seu domínio.
Para cada classe de modelo, escreva pelo menos três testes unitários no diretório test/models/: um que verifica que fromJson cria o objeto corretamente, um que verifica que toJson produz o JSON correto, e um que verifica que copyWith modifica apenas os campos especificados.
Não existe uma estrutura de domínio “correta” — ela depende do problema que o seu grupo está resolvendo. O que importa é que as classes representem fielmente os conceitos do seu negócio, que o código Dart siga as convenções, e que haja testes cobrindo os comportamentos principais.
👉 No moodle, entregue apenas o número do commit que contém o trabalho realizado.
Uma dica prática para modelar o domínio: antes de escrever uma linha de código, esboce um diagrama simples das entidades e como elas se relacionam. Quais são as entidades principais? Quais são os campos de cada uma? Quais entidades se relacionam com quais? Um diagrama de 10 minutos vai evitar refatorações desnecessárias mais tarde.
Perguntas Frequentes deste Módulo
Qual é a diferença entre var, final e late final? A palavra-chave var declara uma variável que pode ser reatribuída. final declara uma variável que só pode ser atribuída uma vez — e essa atribuição precisa acontecer na declaração ou no construtor (para campos de classe). late final declara uma variável final cuja atribuição pode acontecer depois da declaração — útil para campos que são inicializados em métodos como initState() no Flutter. Depois de atribuído, um late final não pode ser reatribuído.
Por que usar const em vez de final? Constantes const são avaliadas em tempo de compilação e compartilhadas em memória — se você criar dois objetos const com os mesmos valores, eles são literalmente o mesmo objeto na memória. No Flutter, isso é significativo para desempenho: widgets const são criados uma única vez e nunca reconstruídos desnecessariamente. Use const sempre que os valores forem conhecidos em tempo de compilação.
Devo usar exceções ou Result<T> para erros? Como regra, use Result<T> para erros que você espera que aconteçam em produção — falhas de rede, dados inválidos, permissões negadas. Use exceções para condições que indicam um bug no código — parâmetros que nunca deveriam ser nulos, estados que não deveriam ser possíveis. Em casos de dúvida, pense: “o usuário pode fazer algo que cause esse erro?” Se sim, Result<T> é mais adequado.
O async/await deixa o código mais lento? Não. O async/await é açúcar sintático sobre o sistema de Future do Dart — ele não adiciona overhead significativo. A diferença de desempenho é imperceptível. O que importa é que código assíncrono nunca bloqueia o thread principal, garantindo que a interface fique responsiva.
Quando devo usar Stream em vez de Future? Use Future quando espera exatamente um resultado (como a resposta de uma requisição HTTP). Use Stream quando espera múltiplos valores ao longo do tempo (como mensagens de chat, atualizações de localização, ou mudanças de estado de conectividade de rede). No Flutter, o padrão StreamBuilder consome Streams diretamente nos widgets.