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á


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.

DART
// 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
TEXT 📖 Somente leitura
> **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


3. Princípios do Sound Null Safety

(1) Sistema de Tipos

100%
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"]
TEXT 📖 Somente leitura
> **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

TEXT 📖 Somente leitura
> **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

DART
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)
}
TEXT 📖 Somente leitura
> **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

TEXT 📖 Somente leitura
> **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

DART
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!
  }
}
TEXT 📖 Somente leitura
> **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

TEXT 📖 Somente leitura
> **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

DART
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');
}
TEXT 📖 Somente leitura
> **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

TEXT 📖 Somente leitura
> **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

DART
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;
}
TEXT 📖 Somente leitura
> **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

TEXT 📖 Somente leitura
> **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

DART
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';
}
TEXT 📖 Somente leitura
> **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

TEXT 📖 Somente leitura
> **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 !

DART
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
}
TEXT 📖 Somente leitura
> **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

TEXT 📖 Somente leitura
> **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 !

DART
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
}
TEXT 📖 Somente leitura
> **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

TEXT 📖 Somente leitura
> **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

DART
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
}
TEXT 📖 Somente leitura
> **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

TEXT 📖 Somente leitura
> **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

DART
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);
}
TEXT 📖 Somente leitura
> **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

DART
// ============================================
// 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();
}
TEXT 📖 Somente leitura
> **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:

TEXT 📖 Somente leitura
=== 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 late em vez de um tipo anulável? R: Use late quando 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), use T?.

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 final local 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, e T? é anulável. Isso garante a segurança de tipos genéricos. T? é na verdade um supertipo de T.

P: Como lidar com muitos campos anuláveis na análise de JSON? R: É recomendado usar ?? para fornecer valores padrão, ou usar o pacote json_serializable para gerar automaticamente um método fromJson com valores padrão. Evite usar ! em todos os lugares.

P: Qual é a diferença entre late final e final? R: final deve ser atribuído na declaração ou na lista de inicializadores do construtor. late final pode ser primeiro atribuído no corpo do construtor ou em um método subsequente. late final adia o momento da atribuição.


📖 Resumo


📝 Exercícios

  1. 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.
  2. Intermediário (Dificuldade ⭐⭐): Projete uma classe Config usando late para adiar a inicialização de 3 campos, definindo-os em um método configure(). Acesse intencionalmente os campos sem chamar configure() e observe o LateInitializationError.
  3. 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 !.

← Lição Anterior | Próxima Lição →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%