Dart: Null Safety no Dart
Última atualização: 2026-08-26
Null Safety é a armadura do Dart — o compilador ajuda você a bloquear 95% dos ataques de ponteiro nulo.
1. O que Você Aprenderá
- Princípios do Sound Null Safety: Tipo anulável
T?vs Tipo não anulávelT - Verificações de null e promoção de tipos
- Palavra-chave late: Inicialização adiada e garantias de segurança
- Casos de uso e riscos do operador
! - Cenário do Bob: Design de Null Safety para configurações opcionais no DataPipeline
2. A História Real de um Desenvolvedor
(1) Dor: NullPointerException é o Crash em Tempo de Execução Mais Comum
O DataPipeline do Bob sofria com crashes em tempo de execução, 60% dos quais eram NullPointerExceptions. Uma API retornava um ID de pedido nulo, um campo opcional estava ausente em um CSV, uma opção de configuração era usada sem ter sido definida — cada situação causava o crash do programa, afetando uma média de 50.000 pedidos por incidente.
(2) A Solução: Sound Null Safety
O Sound Null Safety do Dart 3 divide os tipos em anuláveis T? e não anuláveis T. O compilador garante em tempo de compilação que tipos não anuláveis nunca podem ser null.
// Não anulável: o compilador garante não-nulo
String orderId = 'ORD-001'; // Não pode ser null
// orderId = null; // Erro de compilação!
// Anulável: deve ser verificado antes do uso
String? nickname; // Pode ser null
int length = nickname?.length ?? 0; // Acesso seguro
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
(3) Benefícios
- Exceções de ponteiro nulo são movidas do tempo de execução para o tempo de compilação, reduzindo bugs relacionados em 95%
- Tipos não anuláveis tornam os contratos de API mais claros: quais campos são obrigatórios é imediatamente evidente
- A promoção por verificação de null torna o código mais conciso, eliminando a necessidade de casts manuais de tipo
3. Princípios do Sound Null Safety
(1) Sistema de Tipos
flowchart TD
A[Declaração de Variável] --> B{Pode ser null?}
B -->|Sim| C["T? Tipo anulável"]
B -->|Não| D["T Tipo não anulável"]
C --> E["Deve verificar antes de usar"]
E --> F["if (x != null) → promovido para T"]
C --> G["?? Fornecer valor padrão"]
C --> H["!. Forçar desempacotamento - ARRISCADO"]
D --> I["Usar diretamente - SEGURO"]
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e 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 deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
: Tipos Anuláveis vs Não Anuláveis
void main() {
// Tipos não anuláveis - não podem conter null
String name = 'DataPipeline';
int count = 100;
double amount = 1500.0;
// name = null; // Erro de compilação!
// count = null; // Erro de compilação!
// Tipos anuláveis - podem conter null
String? nickname;
int? maxRetries;
double? discountRate;
print(nickname); // null
print(maxRetries); // null
print(discountRate); // null
// Tipos anuláveis requerem verificação antes do uso
// print(nickname.length); // Erro de compilação! Pode ser null
print(nickname?.length); // null (seguro)
print(nickname?.length ?? 0); // 0 (com padrão)
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
| Tipo | Pode ser null | Verificar antes de usar | Exemplo |
|---|---|---|---|
T |
Não | Não necessário | String name = 'Bob' |
T? |
Sim | Obrigatório | String? name |
4. Verificações de Null e Promoção de Tipos
(1) Promoção de Tipos
Quando o compilador confirma que uma variável anulável não é null, ele automaticamente a promove para um tipo não anulável.
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
: Promoção com verificação if-null
void main() {
String? name = 'Bob';
// Antes da verificação - anulável
// print(name.length); // Erro!
// Após verificação de null - promovido para não anulável
if (name != null) {
print(name.length); // OK! name é promovido para String
print(name.toUpperCase()); // OK!
}
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e 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 deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
: Múltiplos Métodos de Verificação de Null
void main() {
String? city;
// Método 1: verificação if-null
if (city != null) {
print(city.length); // Promovido
}
// Método 2: operador de coalescência nula ??
String safeCity = city ?? 'Unknown';
print(safeCity.length); // Sempre seguro
// Método 3: acesso com consciência de null ?.
int? length = city?.length;
print(length); // null
// Método 4: inicialização late
late String resolvedCity;
resolvedCity = city ?? 'Unknown';
print(resolvedCity.length); // Seguro
// Método 5: assert no modo debug
assert(city != null, 'City must not be null');
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
| Método de Verificação | Sintaxe | Efeito da Promoção | Segurança |
|---|---|---|---|
| if-null | if (x != null) |
Promovido para T | Mais alta |
| ?? | x ?? padrão |
Retorna T | Alta |
| ?. | x?.method() |
Retorna T? | Alta |
| ! | x!.method() |
Tratado como T | Baixa (pode crashar) |
5. A Palavra-chave late
(1) Inicialização Adiada
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
: Uso Básico de late
class DataPipeline {
// late - será inicializado depois, mas garantido antes do uso
late String outputPath;
late final DateTime startTime;
void configure(String path) {
outputPath = path; // Primeira atribuição
}
void start() {
startTime = DateTime.now(); // late final - definido uma vez
print('Pipeline iniciado em $startTime');
}
}
void main() {
final pipeline = DataPipeline();
pipeline.configure('/tmp/reports');
pipeline.start();
// Inicialização late com inicializador
late final int maxRecords = _loadConfig();
// maxRecords é computado apenas no primeiro acesso
print('Máximo de registros: $maxRecords');
}
int _loadConfig() {
print('Carregando configuração...');
return 1000000;
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e 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 deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
: Riscos de late
class RiskyPipeline {
late String config;
void process() {
// Se config não foi definido, lança LateInitializationError
print(config); // Possível crash em tempo de execução!
}
}
void main() {
final pipeline = RiskyPipeline();
// pipeline.process(); // LateInitializationError!
// Padrão seguro: inicializar no construtor
final safePipeline = SafePipeline('/etc/config.yaml');
safePipeline.process(); // OK
}
class SafePipeline {
late String config;
SafePipeline(String configPath) {
config = _loadConfig(configPath);
}
void process() => print('Config: $config');
String _loadConfig(String path) => 'Carregado de $path';
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
Forma de late |
Inicialização | Número de Atribuições | Risco |
|---|---|---|---|
late T |
Antes do uso | Múltiplas | Médio (crash se acessado antes da inicialização) |
late final T |
Antes do uso | 1 | Médio |
late final T = expr |
No primeiro acesso | 1 (automática) | Baixo |
6. O Operador !
(1) Forçar Desempacotamento
! diz ao compilador "Tenho certeza que este valor não é null," pulando a verificação de null. No entanto, se o valor for null, lançará um TypeError.
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
: Usando !
void main() {
String? name = 'Bob';
// Quando você SABE que o valor é não-nulo
print(name!.length); // 3 - OK porque name é 'Bob'
// PERIGOSO: se for null, crasha!
String? maybeNull;
// print(maybeNull!.length); // TypeError em tempo de execução!
// Alternativa segura: use ?? ou verificação if-null
print(maybeNull?.length ?? 0); // 0 - seguro
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
(2) Quando Usar !
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
: Casos de Uso Razáveis para !
class Order {
final String id;
final double amount;
Customer? customer; // Relacionamento opcional
Order({required this.id, required this.amount});
// Use ! quando a lógica de negócio garante não-nulo
String get customerName {
// Isso é arriscado - customer pode ser null
// return customer!.name; // RUIM
// Melhor: acesso seguro com padrão
return customer?.name ?? 'Cliente Desconhecido';
}
}
class Customer {
final String name;
Customer(this.name);
}
void main() {
final order = Order(id: 'ORD-001', amount: 1500.0);
print(order.customerName); // Cliente Desconhecido
// Após definir o customer
order.customer = Customer('Alice');
print(order.customerName); // Alice
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
| Caso de Uso | Recomendado | Não Recomendado |
|---|---|---|
| Variáveis anuláveis | ?. / ?? / if-null |
! |
| Retornos de API | Verificação de null | ! |
.first de coleção |
.firstOrNull ?? padrão |
.first! |
| Após assertion | ! (assertion garante não-nulo) |
— |
7. Null Safety e Coleções
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
: Tratando Null em Coleções
void main() {
// Lista com elementos anuláveis
List<String?> names = ['Alice', null, 'Bob', null, 'Charlie'];
// Filtrar nulls - promoção de tipo funciona
final nonNull = names.whereType<String>().toList();
print(nonNull); // [Alice, Bob, Charlie]
// Map com valores anuláveis
Map<String, double?> revenue = {
'Electronics': 3600.0,
'Books': null,
'Clothing': 890.0,
};
// Filtrar entradas com valores não-nulos
final validRevenue = Map.fromEntries(
revenue.entries.where((e) => e.value != null),
);
print(validRevenue); // {Electronics: 3600.0, Clothing: 890.0}
// Acesso seguro com padrão
final booksRevenue = revenue['Books'] ?? 0;
print(booksRevenue); // 0
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
8. Cenário do Bob: Design de Null Safety para o DataPipeline
▶ Exemplo
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
: Design de Null Safety para Configurações Opcionais
class PipelineConfig {
// Campos obrigatórios - não anuláveis
final String appName;
final String version;
// Campos opcionais - anuláveis com padrões
final String? outputPath;
final String? logLevel;
final double? customTaxRate;
final int? maxRetries;
// Computados a partir de opcionais - não anuláveis
String get effectiveOutputPath => outputPath ?? '/tmp/datapipeline/output';
String get effectiveLogLevel => logLevel ?? 'info';
double get effectiveTaxRate => customTaxRate ?? 0.08;
int get effectiveMaxRetries => maxRetries ?? 3;
const PipelineConfig({
required this.appName,
required this.version,
this.outputPath,
this.logLevel,
this.customTaxRate,
this.maxRetries,
});
String get summary => '''
$appName v$version
Saída: $effectiveOutputPath
Log: $effectiveLogLevel
Imposto: ${(effectiveTaxRate * 100).toStringAsFixed(1)}%
Retries: $effectiveMaxRetries
''';
}
void main() {
// Configuração mínima - apenas campos obrigatórios
final minimal = PipelineConfig(
appName: 'DataPipeline',
version: '1.0.0',
);
print(minimal.summary);
// Configuração completa
final full = PipelineConfig(
appName: 'DataPipeline',
version: '2.0.0',
outputPath: '/data/reports',
logLevel: 'debug',
customTaxRate: 0.10,
maxRetries: 5,
);
print(full.summary);
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
9. Exemplo Completo: Processamento de Pedidos Null-Safe no DataPipeline
// ============================================
// Processamento de Pedidos Null-Safe do DataPipeline
// Demonstração completa de padrões de Null Safety
// ============================================
class Customer {
final String name;
final String? email;
final Address? address;
Customer({required this.name, this.email, this.address});
String get displayName => email ?? name;
String get city => address?.city ?? 'Cidade Desconhecida';
}
class Address {
final String city;
final String? state;
final String country;
Address({required this.city, this.state, required this.country});
String get fullRegion => state != null ? '$city, $state' : city;
}
class Order {
final String id;
final double amount;
final String status;
final Customer? customer;
final String? discountCode;
final double? discountPercent;
Order({
required this.id,
required this.amount,
required this.status,
this.customer,
this.discountCode,
this.discountPercent,
}) : assert(amount > 0, 'Amount must be positive');
double get effectiveDiscount => discountPercent ?? 0;
double get discountedAmount => amount * (1 - effectiveDiscount);
double get tax => discountedAmount * 0.08;
double get total => discountedAmount + tax;
String get customerName => customer?.name ?? 'Convidado';
String get customerCity => customer?.city ?? 'Desconhecido';
String get formattedTotal =>
'\$${total.toStringAsFixed(2)} USD';
@override
String toString() =>
'Order($id, $customerName, ${formattedTotal}, $status)';
}
class OrderProcessor {
final List<Order> _orders = [];
final List<String> _warnings = [];
void addOrder(Order order) {
_orders.add(order);
// Verificações null-safe com avisos
if (order.customer == null) {
_warnings.add('Pedido ${order.id}: Nenhum cliente atribuído');
}
if (order.discountCode != null && order.discountPercent == null) {
_warnings.add('Pedido ${order.id}: Código de desconto sem taxa');
}
}
double get totalRevenue =>
_orders.fold(0.0, (sum, o) => sum + o.total);
List<Order> get ordersWithCustomers =>
_orders.where((o) => o.customer != null).toList();
Map<String, double> revenueByCity() {
final result = <String, double>{};
for (final order in _orders) {
final city = order.customerCity;
result.update(city, (v) => v + order.total, ifAbsent: () => order.total);
}
return result;
}
void printReport() {
print('=== Relatório de Pedidos do DataPipeline ===');
print('Pedidos: ${_orders.length}');
print('Receita: \$${totalRevenue.toStringAsFixed(2)} USD');
print('Com clientes: ${ordersWithCustomers.length}');
print('\nReceita por Cidade:');
for (final entry in revenueByCity().entries) {
print(' ${entry.key}: \$${entry.value.toStringAsFixed(2)} USD');
}
if (_warnings.isNotEmpty) {
print('\nAvisos:');
for (final w in _warnings) {
print(' $w');
}
}
}
}
void main() {
final processor = OrderProcessor();
processor.addOrder(Order(
id: 'ORD-001',
amount: 1500.0,
status: 'completed',
customer: Customer(
name: 'Alice',
email: 'alice@example.com',
address: Address(city: 'New York', state: 'NY', country: 'US'),
),
discountPercent: 0.10,
));
processor.addOrder(Order(
id: 'ORD-002',
amount: 3200.0,
status: 'completed',
// Sem cliente - campo anulável
));
processor.addOrder(Order(
id: 'ORD-003',
amount: 890.0,
status: 'pending',
customer: Customer(
name: 'Bob',
address: Address(city: 'London', country: 'UK'),
),
discountCode: 'SAVE20',
// discountPercent é null - aviso!
));
processor.printReport();
}
> **Saída:** Execute no DartPad local ou com `dart run`. Todos os exemplos deste curso Dart são baseados no Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente dependendo da versão do SDK.
Saída:
=== Relatório de Pedidos do DataPipeline ===
Pedidos: 3
Receita: $5172.36 USD
Com clientes: 2
Receita por Cidade:
New York: $1458.00 USD
Desconhecido: $3456.00 USD
London: $961.20 USD
Avisos:
Pedido ORD-002: Nenhum cliente atribuído
Pedido ORD-003: Código de desconto sem taxa
❓ Perguntas Frequentes
P: Qual é a diferença entre Sound Null Safety e não-Sound? R: Sound Null Safety garante que tipos não anuláveis nunca podem ser null em todo o programa (o compilador realiza verificação global). O modo não-Sound (descontinuado) permite que certos caminhos bypassem as verificações. Dart 3 usa Sound por padrão.
P: Quando devo usar
lateem vez de um tipo anulável? R: Uselatequando você tem certeza que a variável será inicializada antes do uso e não quer que ela seja null. Se a variável realmente pode ser null (semanticamente opcional), useT?.
P: O
!deve ser completamente evitado? R: Não completamente, mas use com moderação. Cenários razoáveis incluem após assertions, em código de teste, ou quando a lógica de negócio garante explicitamente a não-nulidade. Na maioria dos casos,??ou?.é mais seguro.
P: A promoção por verificação de null é eficaz dentro de closures? R: Nem sempre. Se a variável pode ser modificada fora da closure, o compilador não pode garantir a promoção. Capture-a em uma variável
finallocal para resolver isso.
P: Parâmetros de tipo genérico são anuláveis por padrão? R: Não.
Té não anulável por padrão, eT?é anulável. Isso garante a segurança de tipos genéricos.T?é na verdade um supertipo deT.
P: Como lidar com muitos campos anuláveis na análise de JSON? R: É recomendado usar
??para fornecer valores padrão, ou usar o pacotejson_serializablepara gerar automaticamente um métodofromJsoncom valores padrão. Evite usar!em todos os lugares.
P: Qual é a diferença entre
late finalefinal? R:finaldeve ser atribuído na declaração ou na lista de inicializadores do construtor.late finalpode ser primeiro atribuído no corpo do construtor ou em um método subsequente.late finaladia o momento da atribuição.
📖 Resumo
- Sound Null Safety divide os tipos em T (não anulável) e T? (anulável), com garantias do compilador em todo o programa
- Após uma verificação de null, o compilador automaticamente realiza a promoção de tipo, eliminando a necessidade de casts manuais de tipo
latepermite inicialização adiada, mas acessá-lo antes da inicialização causa um crash;late final+ uma expressão de inicialização é o mais seguro- O desempacotamento forçado com
!é arriscado; prefira??ou?.como alternativas - Padrão de design de configuração do DataPipeline: campos obrigatórios são não anuláveis, campos opcionais são anuláveis + getters fornecem valores padrão
📝 Exercícios
- Básico (Dificuldade ⭐): Declare 5 variáveis (2 não anuláveis, 3 anuláveis). Use verificações if-null,
??e?.para acessar com segurança as variáveis anuláveis de três maneiras diferentes, e imprima os resultados. - Intermediário (Dificuldade ⭐⭐): Projete uma classe
Configusandolatepara adiar a inicialização de 3 campos, definindo-os em um métodoconfigure(). Acesse intencionalmente os campos sem chamarconfigure()e observe oLateInitializationError. - Desafio (Dificuldade ⭐⭐⭐): Implemente um cliente API NullSafe onde todas as requisições de rede retornam um tipo
Result<T?>. Trate corretamente três casos: o servidor retorna null, um campo está ausente e uma incompatibilidade de tipo, tudo sem usar!.