Dart não é C# com outra sintaxe
A familiaridade com classes, interfaces e async/await ajuda, mas Dart tem decisões próprias. Código idiomático usa imutabilidade, parâmetros nomeados, construtores concisos e tipos anuláveis explícitos.
final class Product {
const Product({
required this.id,
required this.name,
required this.price,
});
final String id;
final String name;
final double price;
}
final impede uma nova atribuição à variável após a inicialização. const cria valores de tempo de compilação quando toda a expressão também é constante. Prefira imutabilidade por padrão; ela reduz estados intermediários e torna widgets mais previsíveis.
Null safety como contrato
String não aceita null; String? aceita. Essa diferença deve representar o domínio, não apenas silenciar o compilador.
String greeting(String? displayName) {
final normalized = displayName?.trim();
return normalized == null || normalized.isEmpty
? 'Olá, pessoa desenvolvedora'
: 'Olá, $normalized';
}
Use ! somente quando uma invariável já tiver sido verificada e documentada. Em geral, operadores como ?., ?? e promoção de tipo produzem código mais seguro.
Funções e parâmetros nomeados
APIs Flutter usam parâmetros nomeados porque tornam chamadas longas mais legíveis:
Product createProduct({
required String id,
required String name,
double price = 0,
}) {
if (price < 0) {
throw ArgumentError.value(price, 'price', 'não pode ser negativo');
}
return Product(id: id, name: name.trim(), price: price);
}
Parâmetros opcionais devem ter um padrão seguro ou ser anuláveis. Evite booleanos posicionais: save(true, false) comunica pouco; save(validate: true, notify: false) comunica intenção.
Abstrações, genéricos e extensões
abstract interface class Repository<T> {
Future<List<T>> getAll();
Future<void> save(T value);
}
extension CurrencyFormatting on num {
String toBrl() => 'R\$ ${toStringAsFixed(2).replaceAll('.', ',')}';
}
Interfaces definem contratos; genéricos preservam tipo; extensões adicionam operações próximas do domínio sem criar classes utilitárias globais. Mantenha extensões pequenas e sem efeitos colaterais surpreendentes.
Coleções e pattern matching
final products = <Product>[
const Product(id: 'p1', name: 'Notebook', price: 5999),
const Product(id: 'p2', name: 'Mouse', price: 199),
];
final expensiveNames = products
.where((product) => product.price >= 1000)
.map((product) => product.name)
.toList(growable: false);
Listas preservam ordem, Set remove duplicados e Map associa chaves a valores. Escolha a estrutura pelo comportamento necessário, não por hábito.
Pattern matching torna estados fechados mais explícitos:
String statusMessage(Object state) => switch (state) {
LoadingState() => 'Carregando',
SuccessState(:final count) => '$count itens carregados',
FailureState(:final message) => 'Falha: $message',
_ => 'Estado desconhecido',
};
Future, Stream e falhas
Future<T> representa um resultado que chegará uma vez. Stream<T> representa uma sequência de valores ao longo do tempo.
Future<List<Product>> loadProducts(Repository<Product> repository) async {
try {
return await repository.getAll();
} on TimeoutException {
throw const ProductFailure('Tempo de resposta excedido');
} catch (error, stackTrace) {
Error.throwWithStackTrace(
ProductFailure('Não foi possível carregar produtos', cause: error),
stackTrace,
);
}
}
Não capture uma exceção apenas para ignorá-la. Converta falhas técnicas em erros compreensíveis para a camada superior e preserve a causa para diagnóstico.
O que acontece quando Dart executa
Cada aplicação começa em main(). Em desenvolvimento, a Dart VM pode usar compilação JIT para oferecer iteração rápida, depuração e Hot Reload quando integrada ao Flutter. Em produção, alvos compatíveis usam compilação AOT para gerar código otimizado antes da execução. Essa diferença explica por que medir desempenho em debug conduz a conclusões erradas.
Um isolate possui sua própria memória e event loop. Future não cria automaticamente uma thread: ele representa um resultado assíncrono processado pelo fluxo do isolate. Trabalho síncrono pesado continua bloqueando a resposta. Para CPU intensiva, avalie outro isolate e o custo de serializar mensagens; para I/O, APIs assíncronas normalmente são suficientes.
Prática guiada: seu primeiro pacote Dart
No Módulo 1, o Flutter escondeu parte da inicialização da linguagem. Agora você vai trabalhar com Dart sem interface gráfica e construir o núcleo do futuro Sales Management. Você criará primeiro Product; Customer e Order ficam para a prática autônoma, depois de o padrão estar compreendido.
Checkpoint 1 — crie um package, não um arquivo solto
Em uma pasta de estudos fora do projeto do Módulo 1, execute:
dart create --template=package sales_core
cd sales_core
dart test
O template package cria lib/, test/, pubspec.yaml e analysis_options.yaml. O teste inicial deve passar. Se dart não for reconhecido, volte ao diagnóstico do Módulo 1: o Dart SDK acompanha o Flutter SDK.
Substitua lib/sales_core.dart por um arquivo de exportação:
export 'src/in_memory_product_repository.dart';
export 'src/product.dart';
export 'src/product_repository.dart';
Crie a pasta lib/src. A convenção é simples: consumidores importam package:sales_core/sales_core.dart; arquivos internos permanecem organizados em src.
Resultado esperado: dart analyze ainda apresentará erros de URI até os três arquivos exportados serem criados. Neste checkpoint, esse erro é esperado e tem causa conhecida.
Checkpoint 2 — modele uma entidade que protege suas invariantes
Crie lib/src/product.dart:
final class Product {
const Product._({
required this.id,
required this.name,
required this.price,
});
factory Product({
required String id,
required String name,
required double price,
}) {
final normalizedId = id.trim();
final normalizedName = name.trim();
if (normalizedId.isEmpty) {
throw ArgumentError.value(id, 'id', 'não pode ser vazio');
}
if (normalizedName.length < 3) {
throw ArgumentError.value(name, 'name', 'deve ter 3 caracteres');
}
if (price < 0) {
throw ArgumentError.value(price, 'price', 'não pode ser negativo');
}
return Product._(
id: normalizedId,
name: normalizedName,
price: price,
);
}
final String id;
final String name;
final double price;
Product copyWith({String? name, double? price}) {
return Product(
id: id,
name: name ?? this.name,
price: price ?? this.price,
);
}
}
extension CurrencyFormatting on num {
String toBrl() => 'R\$ ${toStringAsFixed(2).replaceAll('.', ',')}';
}
O construtor público é uma factory: ele valida e normaliza antes de criar a instância pelo construtor privado. Assim, não existe Product válido com ID vazio ou preço negativo. copyWith não contorna a validação; ele passa novamente pela factory.
Crie test/product_test.dart:
import 'package:sales_core/sales_core.dart';
import 'package:test/test.dart';
void main() {
group('Product', () {
test('normaliza os textos e formata o preço', () {
final product = Product(id: ' p1 ', name: ' Mouse ', price: 199.9);
expect(product.id, 'p1');
expect(product.name, 'Mouse');
expect(product.price.toBrl(), r'R$ 199,90');
});
test('rejeita preço negativo', () {
expect(
() => Product(id: 'p1', name: 'Mouse', price: -1),
throwsArgumentError,
);
});
});
}
Execute dart test test/product_test.dart. Primeiro você protege o comportamento da entidade; depois adiciona infraestrutura.
Checkpoint 3 — defina o contrato assíncrono
Crie lib/src/product_repository.dart:
import 'product.dart';
abstract interface class ProductRepository {
Future<List<Product>> getAll();
Future<void> save(Product product);
Stream<List<Product>> watchAll();
}
O contrato expressa três comportamentos sem mencionar banco ou HTTP. Future representa um resultado futuro único; Stream representa uma sequência de listas ao longo do tempo.
Crie lib/src/in_memory_product_repository.dart:
import 'dart:async';
import 'product.dart';
import 'product_repository.dart';
final class InMemoryProductRepository implements ProductRepository {
final List<Product> _items = [];
final StreamController<List<Product>> _changes =
StreamController<List<Product>>.broadcast();
List<Product> get _snapshot => List.unmodifiable(_items);
@override
Future<List<Product>> getAll() async => _snapshot;
@override
Future<void> save(Product product) async {
if (_items.any((item) => item.id == product.id)) {
throw StateError('Produto ${product.id} já existe');
}
_items.add(product);
_changes.add(_snapshot);
}
@override
Stream<List<Product>> watchAll() async* {
yield _snapshot;
yield* _changes.stream;
}
Future<void> dispose() => _changes.close();
}
Por que retornar List.unmodifiable? Sem isso, um consumidor poderia executar clear() e alterar o estado interno sem passar por save. Por que broadcast? O exemplo permite mais de um observador. Em produção, a escolha entre stream simples e broadcast deve ser consciente.
Checkpoint 4 — observe uma mudança assíncrona
Crie example/main.dart:
import 'package:sales_core/sales_core.dart';
Future<void> main() async {
final repository = InMemoryProductRepository();
final subscription = repository.watchAll().listen((products) {
final names = products.map((product) => product.name).join(', ');
print('Catálogo: ${names.isEmpty ? 'vazio' : names}');
});
await Future<void>.delayed(Duration.zero);
await repository.save(
Product(id: 'p1', name: 'Notebook', price: 5999),
);
await Future<void>.delayed(Duration.zero);
await subscription.cancel();
await repository.dispose();
}
Execute:
dart run example/main.dart
A saída deve conter primeiro Catálogo: vazio e depois Catálogo: Notebook. await não cria uma thread; ele permite que o isolate continue processando sua fila enquanto aguarda a conclusão assíncrona.
Checkpoint 5 — teste contrato, falha e Stream
Crie test/in_memory_product_repository_test.dart:
import 'package:sales_core/sales_core.dart';
import 'package:test/test.dart';
void main() {
late InMemoryProductRepository repository;
setUp(() => repository = InMemoryProductRepository());
tearDown(() => repository.dispose());
test('salva e devolve uma cópia não modificável', () async {
final product = Product(id: 'p1', name: 'Mouse', price: 199);
await repository.save(product);
final products = await repository.getAll();
expect(products, [same(product)]);
expect(() => products.clear(), throwsUnsupportedError);
});
test('rejeita identificador duplicado', () async {
await repository.save(Product(id: 'p1', name: 'Mouse', price: 199));
expect(
() => repository.save(Product(id: 'p1', name: 'Teclado', price: 299)),
throwsStateError,
);
});
test('publica estado inicial e alteração', () async {
final states = repository.watchAll();
final expectation = expectLater(
states,
emitsInOrder([
isEmpty,
predicate<List<Product>>((items) => items.single.id == 'p1'),
]),
);
await Future<void>.delayed(Duration.zero);
await repository.save(Product(id: 'p1', name: 'Mouse', price: 199));
await expectation;
});
}
O late diz que a variável será inicializada antes do uso; setUp garante isso para cada teste. tearDown fecha o controller mesmo quando um teste falha. expectLater é necessário porque a asserção observa eventos futuros.
dart format .Aplica o formatador oficial aos arquivos do diretório atual. Ele padroniza apresentação, não corrige design ou comportamento. Revise o diff mesmo depois da formatação.
No CI, use dart format --output=none --set-exit-if-changed . para verificar sem reescrever. Um código de saída diferente de zero deve bloquear o gate.
dart analyzeExecuta análise estática a partir do analysis_options.yaml. Leia a severidade e o código do diagnóstico; lints são decisões explícitas do projeto, não obstáculos para silenciar indiscriminadamente.
Análise estática detecta classes inteiras de erro sem executar o programa, mas não confirma regras de negócio.
dart testExecuta os testes do pacote Dart. Uma saída verde significa que as asserções existentes passaram naquele ambiente; não significa que todos os comportamentos relevantes foram cobertos.
Teste valor, falha e transições. Controle relógio, aleatoriedade e I/O para evitar testes intermitentes.
Execute o gate completo:
dart format .
dart analyze
dart test
O módulo está concluído quando os três comandos terminam sem erro e você consegue explicar qual risco cada um reduz.
Erros comuns
| Sintoma | Causa provável | Correção |
|---|---|---|
URI de src/... não existe | arquivo ainda não foi criado ou nome está diferente | compare exports e nomes no disco |
Bad state: Stream has already been listened to | controller não foi criado como broadcast para múltiplos ouvintes | confirme a decisão de broadcast ou mantenha um ouvinte |
| teste do Stream não termina | expectativa foi criada depois do evento ou controller não foi fechado | assine antes de save e use tearDown |
| lista muda fora do repositório | referência mutável vazou | devolva List.unmodifiable |
| exceção aparece depois do teste | Future não foi aguardado | use await ou retorne o Future |
Prática autônoma
Adicione Customer e Order repetindo o processo: invariantes primeiro, contrato depois, implementação por último. Um pedido deve exigir ao menos um item e calcular o total a partir dos produtos; não aceite um total informado externamente sem validação.
Termos para revisar: null safety, Future, Stream, isolate, event loop, JIT, AOT, extension, factory, interface e pattern matching. Consulte a referência de comandos e conceitos sempre que precisar.
Termos para revisar: null safety, Future, Stream, isolate, event loop, JIT, AOT, extension, mixin, sealed class e pattern matching. Consulte a referência de comandos e conceitos sempre que precisar.