Dart: Novos Recursos do Dart 3
Última atualização: 2026-08-26
Os três grandes recursos do Dart 3 — eleve seu código de "funciona" para "elegante."
1. O que Você Aprenderá
- Records: Agregação de dados anônima e leve, múltiplos valores de retorno
- Pattern Matching: desconstrução / if-case / switch-pattern / verificação exaustiva
- Sealed Classes: hierarquia de herança restrita + garantia de switch exaustivo
- O Trio em Ação: A combinação de Sealed + Pattern + Records
- Cenário do Bob: DataPipeline usando classes Sealed para definir tipos de fonte de dados, Pattern matching para analisar resultados
2. A História Real de um Desenvolvedor
(1) Dor: Hierarquia de Tipos Profunda e if-else Verboso
O DataPipeline do Bob tem 3 fontes de dados (API/Arquivo/Banco de dados), cada uma com configurações e métodos de conexão diferentes. Ele as implementou usando herança, mas a hierarquia era profunda e o código verboso. Ao analisar respostas de API, um grande número de verificações if-else era necessário para códigos de status e estruturas de resposta, com 200 linhas de código condicional escritas para apenas 5 tipos de resposta.
(2) Solução com Novos Recursos do Dart 3
Sealed Classes restringem os tipos de fonte de dados, Pattern Matching trata elegantemente várias respostas, e Records fornecem múltiplos valores de retorno leves.
// Classe sealed: tipos de fonte de dados exaustivos
sealed class DataSource {}
class ApiSource extends DataSource { final String endpoint; ApiSource(this.endpoint); }
class FileSource extends DataSource { final String path; FileSource(this.path); }
// Pattern matching em classe sealed - compilador garante que todos os casos são tratados
String describe(DataSource source) => switch (source) {
ApiSource(:final endpoint) => 'API: $endpoint',
FileSource(:final path) => 'Arquivo: $path',
};
// Records: retornar múltiplos valores
(String, double) parseResponse(String raw) => ('ORD-001', 1500.0);
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
(3) Benefícios
- Sealed Classes tornam os ramificações de tipo finitas e enumeráveis; switch exaustivo garante zero omissões.
- Pattern Matching reduz cadeias verbosas de if-else a expressões switch declarativas.
- Records substituem pequenas classes; múltiplos valores de retorno não precisam mais de uma biblioteca Tuple ou um Map.
3. Records
(1) Fundamentos de Record
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
: Criando e Usando Records
void main() {
// Record posicional
var order = ('ORD-001', 1500.0, 'completed');
print(order.$1); // ORD-001
print(order.$2); // 1500.0
print(order.$3); // completed
// Record nomeado (preferido para legibilidade)
var order2 = (id: 'ORD-002', amount: 3200.0, status: 'pending');
print(order2.id); // ORD-002
print(order2.amount); // 3200.0
print(order2.status); // pending
// Mistura posicional e nomeado
var mixed = ('ORD-003', 890.0, category: 'Electronics');
print(mixed.$1); // ORD-003
print(mixed.category); // Electronics
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
: Records como Valores de Retorno de Função
// Múltiplos valores de retorno com records
({String id, double amount, double tax}) calculateOrder(double amount, double taxRate) {
return (
id: 'ORD-${DateTime.now().millisecondsSinceEpoch}',
amount: amount,
tax: amount * taxRate,
);
}
// Record posicional para agrupamento rápido
(String, double) parseAmount(String input) {
final parts = input.split(':');
return (parts[0], double.parse(parts[1]));
}
void main() {
final order = calculateOrder(1500.0, 0.08);
print('ID: ${order.id}, Valor: \$${order.amount}, Imposto: \$${order.tax}');
final (label, value) = parseAmount('Revenue:52500.75');
print('$label: \$$value USD');
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
| Recurso de Record | Sintaxe | Descrição |
|---|---|---|
| Campos posicionais | .$1, .$2 |
Acesso por posição |
| Campos nomeados | .name |
Acesso por nome |
| Anotação de tipo | (String, int) ou ({String name, int count}) |
Tipo explícito |
| Igualdade | Igualdade por valor | Iguais se os valores dos campos forem os mesmos |
4. Pattern Matching
(1) Tipos de Pattern Matching
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
: Padrões de Variáveis e Desconstrução
void main() {
// Padrão de variável - extrair valores
var (id, amount, status) = ('ORD-001', 1500.0, 'completed');
print('ID: $id, Valor: $amount, Status: $status');
// Desconstrução de record com campos nomeados
var (:id, :amount, :status) = (id: 'ORD-002', amount: 3200.0, status: 'pending');
print('ID: $id, Valor: $amount, Status: $status');
// Desconstrução de lista
var [first, second, ...rest] = [1, 2, 3, 4, 5];
print('Primeiro: $first, Segundo: $second, Resto: $rest');
// Desconstrução de mapa
var {'id': orderId, 'amount': orderAmount} = {'id': 'ORD-003', 'amount': 890.0};
print('Pedido: $orderId, Valor: $orderAmount');
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
: Padrão if-case
void main() {
Object value = '1,500.00 USD';
// Pattern matching if-case
if (value case String s when s.contains('USD')) {
print('Valor em USD: $s');
}
// Pattern matching em record
var response = (statusCode: 200, body: '{"orders": 1500}');
if (response case (statusCode: 200, :var body)) {
print('Resposta de sucesso: $body');
}
// Pattern matching em lista
var orders = ['ORD-001', 'ORD-002', 'ORD-003'];
if (orders case [var first, var second, ...]) {
print('Primeiros dois: $first, $second');
}
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
: Padrão switch
String classifyOrder(Object value) => switch (value) {
// Padrão de tipo
int i when i > 1000 => 'Inteiro de alto valor: $i',
int i => 'Inteiro de baixo valor: $i',
double d when d >= 1000 => 'Premium: \$${d.toStringAsFixed(2)}',
double d => 'Padrão: \$${d.toStringAsFixed(2)}',
String s => 'String: $s',
List l when l.length > 100 => 'Lote grande: ${l.length} itens',
List l => 'Lote pequeno: ${l.length} itens',
_ => 'Tipo desconhecido',
};
// Pattern matching com records
String describeResponse((int, String) response) => switch (response) {
(200, var body) => 'OK: $body',
(404, _) => 'Não Encontrado',
(500, var msg) => 'Erro no Servidor: $msg',
(>= 400, var msg) => 'Erro do Cliente: $msg',
_ => 'Resposta desconhecida',
};
void main() {
print(classifyOrder(1500)); // Inteiro de alto valor: 1500
print(classifyOrder(890.0)); // Padrão: $890.00
print(classifyOrder([1, 2, 3])); // Lote pequeno: 3 itens
print(describeResponse((200, 'OK'))); // OK: OK
print(describeResponse((404, 'Missing'))); // Não Encontrado
print(describeResponse((500, 'Crash'))); // Erro no Servidor: Crash
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
| Tipo de Padrão | Sintaxe | Uso |
|---|---|---|
| Padrão de variável | var x |
Extrair valores |
- Padrão de tipo |
Type x| Verificação de tipo + extração | | Padrão de constante |42,'hello'| Correspondência exata | | Padrão relacional |>= 1000| Verificação de intervalo | | Padrão lógico |a \|\| b,a && b| Combinar condições | | Guarda |when condition| Condição adicional | | Curinga |_| Ignorar valor |
5. Sealed Classes
(1) Hierarquia de Classes Sealed
classDiagram
class DataSource {
<<sealed>>
}
class ApiSource {
+String endpoint
+Map headers
}
class FileSource {
+String path
+Encoding encoding
}
class DatabaseSource {
+String connectionString
}
DataSource <|-- ApiSource
DataSource <|-- FileSource
DataSource <|-- DatabaseSource
note for DataSource "switch exaustivo garantido"
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
: Definição de Classe Sealed e Correspondência Exaustiva
sealed class DataSource {
const DataSource();
}
class ApiSource extends DataSource {
final String endpoint;
final Map<String, String> headers;
const ApiSource(this.endpoint, {this.headers = const {}});
}
class FileSource extends DataSource {
final String path;
const FileSource(this.path);
}
class DatabaseSource extends DataSource {
final String connectionString;
const DatabaseSource(this.connectionString);
}
// Switch exaustivo - compilador verifica todos os subtipos
String describeSource(DataSource source) => switch (source) {
ApiSource(:final endpoint, :final headers) =>
'API: $endpoint (${headers.length} headers)',
FileSource(:final path) =>
'Arquivo: $path',
DatabaseSource(:final connectionString) =>
'Banco de dados: $connectionString',
};
// Se você adicionar um novo subtipo e esquecer de atualizar o switch,
// o compilador reportará um erro!
void main() {
final sources = <DataSource>[
ApiSource('https://api.example.com/orders', headers: {'Authorization': 'Bearer token'}),
FileSource('/data/orders.csv'),
DatabaseSource('postgresql://localhost:5432/orders'),
];
for (final source in sources) {
print(describeSource(source));
}
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
| Recurso de Classe Sealed | Descrição |
|---|---|
| Restrição de subclasses | Devem ser definidas na mesma biblioteca |
| Verificação exaustiva | switch deve cobrir todos os subtipos |
| Garantia em tempo de compilação | Switch omitido causará erro de compilação quando nova subclasse for adicionada |
| Não pode ser instanciada | É implicitamente abstrata |
6. O Trio em Ação
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
: Combinação Sealed + Pattern + Records
// Classe sealed para resultados de API
sealed class ApiResult<T> {
const ApiResult();
}
class Success<T> extends ApiResult<T> {
final T data;
final (int, String) metadata; // Record: (statusCode, message)
const Success(this.data, this.metadata);
}
class ApiError<T> extends ApiResult<T> {
final String message;
final int? statusCode;
const ApiError(this.message, {this.statusCode});
}
class NetworkError<T> extends ApiResult<T> {
final String reason;
const NetworkError(this.reason);
}
// Pattern matching com desconstrução
String handleResult(ApiResult<List<String>> result) => switch (result) {
Success(:final data, metadata: (200, final msg)) =>
'OK ($msg): ${data.length} itens carregados',
Success(:final data, metadata: (final code, _)) =>
'Carregado com status $code: ${data.length} itens',
ApiError(:final message, statusCode: final code?) =>
'Erro de API [$code]: $message',
ApiError(:final message) =>
'Erro de API: $message',
NetworkError(:final reason) =>
'Erro de Rede: $reason',
};
void main() {
final results = <ApiResult<List<String>>>[
Success(['ORD-001', 'ORD-002'], (200, 'OK')),
Success(['ORD-003'], (206, 'Partial Content')),
ApiError('Rate limit exceeded', statusCode: 429),
ApiError('Erro desconhecido'),
NetworkError('Tempo limite de conexão esgotado'),
];
for (final result in results) {
print(handleResult(result));
}
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
7. Cenário do Bob: Fontes de Dados e Análise de Resultados no DataPipeline
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
: Abstração Completa de Fonte de Dados
sealed class DataSource {
const DataSource();
String get displayName;
}
class ApiSource extends DataSource {
final String endpoint;
final Duration timeout;
const ApiSource(this.endpoint, {this.timeout = const Duration(seconds: 10)});
@override
String get displayName => 'API ($endpoint)';
}
class FileSource extends DataSource {
final String path;
final String format;
const FileSource(this.path, {this.format = 'csv'});
@override
String get displayName => 'Arquivo ($path, $format)';
}
class DatabaseSource extends DataSource {
final String connectionString;
final String query;
const DatabaseSource(this.connectionString, {this.query = 'SELECT * FROM orders'});
@override
String get displayName => 'Banco de dados ($connectionString)';
}
// Resultado do processamento com Records
typedef ProcessingResult = ({int processed, int skipped, double revenue, Duration time});
ProcessingResult processDataSource(DataSource source) => switch (source) {
ApiSource(:final endpoint, :final timeout) => (
processed: 50000,
skipped: 120,
revenue: 525000.0,
time: timeout,
),
FileSource(:final path, :final format) => (
processed: 100000,
skipped: 350,
revenue: 1200000.0,
time: Duration(seconds: 5),
),
DatabaseSource(:final connectionString, :final query) => (
processed: 1200000,
skipped: 800,
revenue: 15000000.0,
time: Duration(seconds: 15),
),
};
void main() {
final sources = <DataSource>[
ApiSource('https://api.example.com/orders'),
FileSource('/data/orders.csv'),
DatabaseSource('postgresql://localhost:5432/orders'),
];
print('=== Análise de Fontes do DataPipeline ===');
for (final source in sources) {
final (:processed, :skipped, :revenue, :time) = processDataSource(source);
print('\n${source.displayName}:');
print(' Processados: ${processed} pedidos');
print(' Ignorados: $skipped registros');
print(' Receita: \$${revenue.toStringAsFixed(2)} USD');
print(' Tempo: ${time.inSeconds}s');
}
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
8. Exemplo Completo: Sistema de Processamento de Respostas do DataPipeline
// ============================================
// Sistema de Processamento de Respostas do DataPipeline
// Classes Sealed + Pattern matching + Records
// ============================================
// Classe sealed para diferentes tipos de resposta
sealed class ApiResponse {
const ApiResponse();
}
class SuccessResponse extends ApiResponse {
final int statusCode;
final Map<String, dynamic> data;
const SuccessResponse(this.statusCode, this.data);
}
class ErrorResponse extends ApiResponse {
final int statusCode;
final String message;
final String? details;
const ErrorResponse(this.statusCode, this.message, {this.details});
}
class TimeoutResponse extends ApiResponse {
final Duration timeout;
final String endpoint;
const TimeoutResponse(this.timeout, this.endpoint);
}
class RedirectResponse extends ApiResponse {
final String newEndpoint;
final int statusCode;
const RedirectResponse(this.newEndpoint, this.statusCode);
}
// Resultado do processamento como um Record
typedef Outcome = ({bool success, String message, double? revenue});
// Pattern matching em todos os tipos de resposta - exaustivo!
Outcome handleResponse(ApiResponse response) => switch (response) {
SuccessResponse(statusCode: 200, :final data) when data.containsKey('orders') => (
success: true,
message: 'Carregados ${data['orders']} pedidos',
revenue: (data['revenue'] as num?)?.toDouble(),
),
SuccessResponse(statusCode: 200, :final data) => (
success: true,
message: 'OK mas sem campo orders: ${data.keys}',
revenue: null,
),
SuccessResponse(statusCode: 206, :final data) => (
success: true,
message: 'Dados parciais: ${data.length} campos',
revenue: null,
),
SuccessResponse(statusCode: final code) => (
success: true,
message: 'Código de sucesso inesperado: $code',
revenue: null,
),
ErrorResponse(statusCode: 429, :final message) => (
success: false,
message: 'Limite de taxa atingido: $message',
revenue: null,
),
ErrorResponse(statusCode: final code, :final message, :final details?) => (
success: false,
message: 'Erro [$code]: $message - $details',
revenue: null,
),
ErrorResponse(statusCode: final code, :final message) => (
success: false,
message: 'Erro [$code]: $message',
revenue: null,
),
TimeoutResponse(:final timeout, :final endpoint) => (
success: false,
message: 'Timeout após ${timeout.inSeconds}s em $endpoint',
revenue: null,
),
RedirectResponse(:final newEndpoint, :final statusCode) => (
success: false,
message: 'Redireção ($statusCode) para $newEndpoint',
revenue: null,
),
};
void main() {
final responses = <ApiResponse>[
SuccessResponse(200, {'orders': 50000, 'revenue': 525000.0}),
SuccessResponse(200, {'products': 3000}),
SuccessResponse(206, {'partial': true}),
ErrorResponse(429, 'Limite de taxa excedido'),
ErrorResponse(500, 'Erro interno do servidor', details: 'Conexão com banco de dados perdida'),
ErrorResponse(404, 'Não encontrado'),
TimeoutResponse(const Duration(seconds: 30), '/api/v1/orders'),
RedirectResponse('/api/v2/orders', 301),
];
print('=== Gerenciador de Respostas do DataPipeline ===\n');
for (final response in responses) {
final (:success, :message, :revenue) = handleResponse(response);
final status = success ? 'OK' : 'FALHA';
final rev = revenue != null ? ' (\$${revenue.toStringAsFixed(2)} USD)' : '';
print('[$status] $message$rev');
}
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos do curso Dart são baseados no Dart 3.x / Flutter 3.x; os resultados podem variar ligeiramente dependendo da versão do SDK.
Saída:
=== Gerenciador de Respostas do DataPipeline ===
[OK] Carregados 50000 pedidos ($525000.00 USD)
[OK] OK mas sem campo orders: (products)
[OK] Dados parciais: 1 campos
[FALHA] Limite de taxa atingido: Limite de taxa excedido
[FALHA] Erro [500]: Erro interno do servidor - Conexão com banco de dados perdida
[FALHA] Erro [404]: Não encontrado
[FALHA] Timeout após 30s em /api/v1/orders
[FALHA] Redireção (301) para /api/v2/orders
❓ Perguntas Frequentes
P: Qual é a diferença entre Records e Classes? R: Records são estruturas de dados anônimas, baseadas em valor e leves; Classes têm nomes, são baseadas em referência e podem ter métodos. Records são adequados para passagem simples de dados, enquanto Classes são para lógica de negócio complexa.
P: Qual é a diferença entre uma classe Sealed e uma classe abstrata? R: Classes Sealed restringem subclasses à mesma biblioteca, permitindo que o compilador realize verificações exaustivas; classes abstratas podem ter subclasses definidas em qualquer lugar, impossibilitando exaustividade. Use sealed quando precisar de garantias exaustivas.
P: Onde Pattern matching pode ser usado? R: Expressões switch, instruções switch, if-case, declarações de variáveis, loops for-in. No Dart 3, padrões estão em todo lugar.
P: Qual é o desempenho de Records? R: Records são compilados para objetos comuns, com desempenho comparável a pequenas classes. Não há overhead extra, mas também não são mais rápidos que classes. Escolha baseado em legibilidade.
P: Uma classe sealed pode ter construtor? R: Pode ter construtores factory (que retornam subclasses), mas a classe em si não pode ser instanciada (é implicitamente abstrata). Construtores são principalmente para compartilhar lógica de inicialização entre subclasses.
P: Existe diferença entre
_edefaultem uma expressão switch? R: Funcionalmente, são os mesmos — ambos são correspondências curinga (catch-all). Dart 3 recomenda usar_(padrão curinga) para concisão.defaulté a sintaxe legada mantida para compatibilidade.
P: Records podem ser usados como chaves de um Map? R: Sim. Records são baseados em igualdade de valor e implementam automaticamente == e hashCode.
(1, 2) == (1, 2)é true.
📖 Resumo
- Records são agregação de dados anônima e leve com suporte a campos posicionais e nomeados, e igualdade baseada em valor.
- Pattern Matching suporta if-case, switch-pattern, desconstrução de variáveis e condições de guarda.
- Sealed Classes restringem subclasses à mesma biblioteca; o compilador garante um switch exaustivo.
- O Trio em Ação: Sealed define um tipo finito → Pattern faz correspondência e desconstrução → Records fornecem valores de retorno leves.
- Esta é a atualização mais central do Dart 3, tornando o código mais seguro, mais conciso e mais expressivo.
📝 Exercícios
- Básico (Dificuldade ⭐): Defina dois Records: um record posicional
(String, double)representando ID e valor do pedido, e um record nomeado({String id, double amount, String status}). Crie instâncias e desconstrua/imprima-as. - Intermediário (Dificuldade ⭐⭐): Use uma Classe Sealed para definir 3 métodos de pagamento (CreditCard/PayPal/BankTransfer), cada um com campos diferentes. Implemente um método
describePayment()usando uma expressão switch, garantindo exaustividade. - Desafio (Dificuldade ⭐⭐⭐): Projete um sistema completo de processamento de respostas de API: Use uma Classe Sealed para definir tipos de resposta (Success/ValidationError/ServerError/Timeout), cada um carregando dados Record diferentes. Implemente um gerenciador de respostas usando Pattern Matching, incorporando padrões relacionais e condições de guarda.