Dart: Dart Stream
Última atualização: 2026-08-26
Stream é um rio de dados — contínuo, processado à medida que chega, sem necessidade de esperar que tudo chegue de uma vez.
1. O que Você Aprenderá
- Conceitos de Stream: Subscrição única vs Broadcast
- Criação de Stream: fromIterable / periodic / transformação de eventos
- Operadores: map / where / expand / take / skip / distinct
- StreamController e StreamSubscription
- Cenário do Bob: Processamento de fluxo de pedidos em tempo real no DataPipeline
2. A História Real de um Desenvolvedor
(1) Dor: Milhões de Registros de Dados Não Podem Ser Carregados na Memória de Uma Só Vez
O DataPipeline do Bob inicialmente carregava todos os 1.200.000 pedidos na memória para processamento ao lidar com pedidos de e-commerce, resultando em um pico de memória de 4GB e causando o crash do servidor com erro de OOM três vezes. Além disso, conforme pedidos em tempo real continuavam a chegar, o modelo de processamento em lote era incapaz de lidar com os dados recém-chegados.
(2) Solução com Stream
Stream permite que os dados sejam processados um a um como um fluxo de água: processe um assim que chegar, mantendo o uso de memória estável em 50MB. Operadores de Stream (where/map/reduce) permitem definir declarativamente o pipeline de processamento.
orderStream
.where((order) => order.status == 'completed')
.map((order) => order.amount)
.fold(0, (sum, amount) => sum + amount)
.then((total) => print('Receita: \$$total USD'));
> **Saída:** Execute localmente no DartPad 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
- Memória reduzida de 4GB para 50MB, eliminando erros de OOM
- Processamento em tempo real de pedidos recém-chegados, latência reduzida de "aguardando processamento em lote" para "segundos"
- Cadeias de operadores de Stream tornam a lógica de processamento de dados declarativa e composável
3. Fundamentos de Stream
(1) Subscrição Única vs Broadcast
flowchart LR
A[Fonte de Dados] --> B["StreamController"]
B --> C["where() Filtrar"]
C --> D["map() Transformar"]
D --> E["expand() Achatar"]
E --> F["take() Limitar"]
F --> G["listen() Consumir"]
subgraph Pipeline de Stream
C
D
E
F
end
> **Saída:** Execute localmente no DartPad 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 | Inscritos | Reproduzível | Caso de Uso |
|---|---|---|---|
| Stream de subscrição única | 1 | Não reproduzível | Leitura de arquivos, respostas HTTP |
| Stream broadcast | Múltiplos | Não reproduzível | Eventos de UI, dados em tempo real |
▶ Exemplo
> **Saída:** Execute localmente no DartPad 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.
:Fundamentos de Stream
import 'dart:async';
void main() async {
// Criar stream a partir de iterável
final stream = Stream.fromIterable([1, 2, 3, 4, 5]);
// Escutar stream
await for (final value in stream) {
print('Recebido: $value');
}
// Alternativa: escutar com callback
final stream2 = Stream.fromIterable(['ORD-001', 'ORD-002', 'ORD-003']);
stream2.listen(
(order) => print('Pedido: $order'),
onDone: () => print('Stream concluída'),
onError: (e) => print('Erro: $e'),
);
// Aguardar stream completar
await stream2.drain();
}
> **Saída:** Execute localmente no DartPad 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.
4. Criação de Stream
▶ Exemplo
> **Saída:** Execute localmente no DartPad 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.
:Criação com fromIterable
import 'dart:async';
void main() async {
// A partir de iterável - emite cada elemento
final orders = Stream.fromIterable([
{'id': 'ORD-001', 'amount': 1500.0},
{'id': 'ORD-002', 'amount': 3200.0},
{'id': 'ORD-003', 'amount': 890.0},
]);
await for (final order in orders) {
print('Pedido: ${order['id']} - \$${order['amount']} USD');
}
}
> **Saída:** Execute localmente no DartPad 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 localmente no DartPad 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.
:Criação com periodic
import 'dart:async';
void main() async {
// Stream periódica - emite valores em intervalos regulares
final ticker = Stream.periodic(
const Duration(seconds: 1),
(count) => 'Tick ${count + 1}',
);
// Pegar apenas os primeiros 5 ticks
await for (final tick in ticker.take(5)) {
print(tick);
}
}
> **Saída:** Execute localmente no DartPad 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 localmente no DartPad 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.
:Criação com StreamController
import 'dart:async';
void main() async {
final controller = StreamController<String>();
// Adicionar dados ao stream
controller.add('ORD-001');
controller.add('ORD-002');
controller.add('ORD-003');
// Fechar o stream quando terminar
controller.close();
// Consumir o stream
await for (final order in controller.stream) {
print('Processando: $order');
}
}
> **Saída:** Execute localmente no DartPad 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.
| Método de Criação | Sintaxe | Fonte de Dados | Caso de Uso |
|---|---|---|---|
| fromIterable | Stream.fromIterable(list) |
Coleção existente | Testes, conjuntos pequenos |
| periodic | Stream.periodic(duration, fn) |
Geração temporizada | Timers, polling |
| StreamController | StreamController<T>() |
Adicionado manualmente | Fontes de eventos, dados em tempo real |
| empty | Stream.empty() |
Nenhum | Stream vazia |
| value | Stream.value(v) |
Valor único | Envolver um único valor |
5. Operadores de Stream
▶ Exemplo
> **Saída:** Execute localmente no DartPad 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.
:where — Filtragem
import 'dart:async';
void main() async {
final amounts = Stream.fromIterable([1500.0, 50.0, 3200.0, 890.0, -100.0]);
// Filtrar apenas valores positivos >= 100
final highValue = amounts.where((a) => a >= 100);
await for (final amount in highValue) {
print('\$${amount.toStringAsFixed(2)} USD');
}
// Saída: $1500.00 USD, $3200.00 USD, $890.00 USD
}
> **Saída:** Execute localmente no DartPad 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 localmente no DartPad ou with `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.
:map — Transformação
import 'dart:async';
void main() async {
final amounts = Stream.fromIterable([1500.0, 3200.0, 890.0]);
// Transformar valores em strings formatadas
final formatted = amounts.map((a) => '\$${a.toStringAsFixed(2)} USD');
await for (final text in formatted) {
print(text);
}
// Saída: $1500.00 USD, $3200.00 USD, $890.00 USD
}
> **Saída:** Execute localmente no DartPad 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 localmente no DartPad 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.
:expand — Achatar
import 'dart:async';
void main() async {
final orders = Stream.fromIterable([
{'id': 'ORD-001', 'items': ['Laptop', 'Mouse']},
{'id': 'ORD-002', 'items': ['Keyboard']},
]);
// Expand: cada pedido → múltiplos itens
final items = orders.expand((order) =>
(order['items'] as List).cast<String>());
await for (final item in items) {
print('Item: $item');
}
// Saída: Item: Laptop, Item: Mouse, Item: Keyboard
}
> **Saída:** Execute localmente no DartPad 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 localmente no DartPad 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.
:take / skip / distinct
import 'dart:async';
void main() async {
final data = Stream.fromIterable([1, 1, 2, 2, 3, 3, 4, 5]);
// Pegar os primeiros 3 elementos
print('take(3):');
await data.take(3).forEach(print); // 1, 1, 2
// Pular os primeiros 2
print('skip(2):');
await Stream.fromIterable([1, 1, 2, 2, 3]).skip(2).forEach(print); // 2, 2, 3
// Remover duplicatas consecutivas
print('distinct:');
await Stream.fromIterable([1, 1, 2, 2, 3, 3]).distinct().forEach(print); // 1, 2, 3
}
> **Saída:** Execute localmente no DartPad 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.
| Operador | Função | Tipo de Retorno |
|---|---|---|
where |
Filtrar elementos | Stream<T> |
map |
Transformar elementos | Stream<R> |
expand |
Achatar um-para-muitos | Stream<T> |
take |
Pegar primeiros N elementos | Stream<T> |
skip |
Pular primeiros N elementos | Stream<T> |
distinct |
Remover duplicatas consecutivas | Stream<T> |
takeWhile |
Pegar enquanto condição for verdadeira | Stream<T> |
skipWhile |
Pular enquanto condição for verdadeira | Stream<T> |
6. StreamController e StreamSubscription
(1) StreamController
▶ Exemplo
> **Saída:** Execute localmente no DartPad 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.
:Uso Completo do StreamController
import 'dart:async';
class OrderStream {
final _controller = StreamController<Map<String, dynamic>>();
Stream<Map<String, dynamic>> get stream => _controller.stream;
void addOrder(Map<String, dynamic> order) => _controller.add(order);
void addError(Object error) => _controller.addError(error);
void close() => _controller.close();
}
void main() async {
final orderStream = OrderStream();
// Inscrever-se no stream
final subscription = orderStream.stream.listen(
(order) => print('Processando: ${order['id']}'),
onError: (e) => print('Erro: $e'),
onDone: () => print('Stream fechada'),
);
// Adicionar pedidos
orderStream.addOrder({'id': 'ORD-001', 'amount': 1500.0});
orderStream.addOrder({'id': 'ORD-002', 'amount': 3200.0});
orderStream.addOrder({'id': 'ORD-003', 'amount': 890.0});
// Fechar o stream
orderStream.close();
// Aguardar stream terminar
await subscription.asFuture();
}
> **Saída:** Execute localmente no DartPad 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.
(2) StreamController Broadcast
▶ Exemplo
> **Saída:** Execute localmente no DartPad 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.
:Modo Broadcast
import 'dart:async';
void main() async {
// Controller broadcast - múltiplos ouvintes
final controller = StreamController<String>.broadcast();
// Múltiplos inscritos
final sub1 = controller.stream.listen((e) => print('Sub1: $e'));
final sub2 = controller.stream.listen((e) => print('Sub2: $e'));
// Ambos os inscritos recebem o mesmo evento
controller.add('ORD-001');
controller.add('ORD-002');
controller.close();
await Future.delayed(const Duration(milliseconds: 100));
}
> **Saída:** Execute localmente no DartPad 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: Processamento de Fluxo de Pedidos em Tempo Real
▶ Exemplo
> **Saída:** Execute localmente no DartPad 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.
:Pipeline de Stream Processando Pedidos
import 'dart:async';
class Order {
final String id;
final double amount;
final String status;
final String category;
Order({required this.id, required this.amount, required this.status, required this.category});
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)}, $status, $category)';
}
void main() async {
// Simular stream de pedidos em tempo real
final controller = StreamController<Order>();
// Construir pipeline de processamento
final revenueByCategory = <String, double>{};
var totalOrders = 0;
var completedOrders = 0;
controller.stream
.where((order) => order.amount > 0) // Filtrar inválidos
.where((order) => order.status == 'completed') // Apenas concluídos
.map((order) => order) // Poderia transformar aqui
.listen(
(order) {
completedOrders++;
totalOrders++;
revenueByCategory.update(
order.category,
(v) => v + order.amount,
ifAbsent: () => order.amount,
);
print(' Processado: ${order.id} - \$${order.amount.toStringAsFixed(2)} USD');
},
onDone: () {
print('\n=== Processamento do Stream Concluído ===');
print('Pedidos concluídos: $completedOrders');
for (final entry in revenueByCategory.entries) {
print(' ${entry.key}: \$${entry.value.toStringAsFixed(2)} USD');
}
final total = revenueByCategory.values.fold(0.0, (a, b) => a + b);
print('Receita total: \$${total.toStringAsFixed(2)} USD');
},
);
// Simular pedidos chegando
controller.add(Order(id: 'ORD-001', amount: 1500.0, status: 'completed', category: 'Electronics'));
controller.add(Order(id: 'ORD-002', amount: -50.0, status: 'completed', category: 'Books')); // Inválido
controller.add(Order(id: 'ORD-003', amount: 3200.0, status: 'pending', category: 'Electronics')); // Não concluído
controller.add(Order(id: 'ORD-004', amount: 890.0, status: 'completed', category: 'Clothing'));
controller.add(Order(id: 'ORD-005', amount: 2100.0, status: 'completed', category: 'Electronics'));
controller.close();
await Future.delayed(const Duration(milliseconds: 100));
}
> **Saída:** Execute localmente no DartPad 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: Pipeline de Stream do DataPipeline
// ============================================
// Pipeline de Processamento de Stream do DataPipeline
// Stream de pedidos em tempo real com operadores
// ============================================
import 'dart:async';
class Order {
final String id;
final double amount;
final String status;
final String category;
final String region;
const Order({
required this.id,
required this.amount,
required this.status,
required this.category,
this.region = 'US',
});
double get taxAmount => amount * 0.08;
double get totalWithTax => amount + taxAmount;
@override
String toString() =>
'Order($id, \$${amount.toStringAsFixed(2)}, $status, $category, $region)';
}
class StreamPipeline {
final _controller = StreamController<Order>();
final Map<String, double> _revenue = {};
final Map<String, int> _count = {};
int _totalProcessed = 0;
int _totalSkipped = 0;
late StreamSubscription<Order> _subscription;
Stream<Order> get stream => _controller.stream;
StreamPipeline() {
_subscription = _controller.stream
.where((o) => o.amount > 0)
.where((o) => o.status != 'cancelled')
.distinct((o) => o.id) // Deduplicar por ID
.listen(
_processOrder,
onError: (e) => print('Erro no pipeline: $e'),
onDone: _printSummary,
);
}
void _processOrder(Order order) {
_totalProcessed++;
_revenue.update(
order.category,
(v) => v + order.totalWithTax,
ifAbsent: () => order.totalWithTax,
);
_count.update(order.category, (v) => v + 1, ifAbsent: () => 1);
}
void addOrder(Order order) => _controller.add(order);
void addError(Object error) => _controller.addError(error);
Future<void> close() async {
await _controller.close();
await _subscription.asFuture();
}
void _printSummary() {
final totalRevenue = _revenue.values.fold(0.0, (a, b) => a + b);
final totalCount = _count.values.fold(0, (a, b) => a + b);
print('\n=== Relatório de Stream do DataPipeline ===');
print('Processados: $_totalProcessed pedidos');
print('Ignorados: $_totalSkipped registros');
print('Receita: \$${totalRevenue.toStringAsFixed(2)} USD');
print('Pedidos: $totalCount');
print('\n--- Por Categoria ---');
final sorted = _revenue.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
for (final entry in sorted) {
final count = _count[entry.key] ?? 0;
final avg = entry.value / count;
print(' ${entry.key}:');
print(' Pedidos: $count');
print(' Receita: \$${entry.value.toStringAsFixed(2)} USD');
print(' Média: \$${avg.toStringAsFixed(2)} USD');
}
}
}
void main() async {
final pipeline = StreamPipeline();
// Simular stream de pedidos em tempo real
final orders = [
Order(id: 'ORD-001', amount: 1500.0, status: 'completed', category: 'Electronics'),
Order(id: 'ORD-002', amount: -50.0, status: 'completed', category: 'Books'),
Order(id: 'ORD-003', amount: 3200.0, status: 'pending', category: 'Electronics'),
Order(id: 'ORD-004', amount: 890.0, status: 'completed', category: 'Clothing'),
Order(id: 'ORD-001', amount: 1500.0, status: 'completed', category: 'Electronics'), // Duplicado
Order(id: 'ORD-005', amount: 2100.0, status: 'completed', category: 'Electronics'),
Order(id: 'ORD-006', amount: 500.0, status: 'cancelled', category: 'Books'),
Order(id: 'ORD-007', amount: 120.0, status: 'completed', category: 'Books'),
];
// Emitir pedidos um a um (simulando tempo real)
for (final order in orders) {
pipeline.addOrder(order);
}
await pipeline.close();
}
> **Saída:** Execute localmente no DartPad 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:
=== Relatório de Stream do DataPipeline ===
Processados: 5 pedidos
Ignorados: 0 registros
Receita: $8154.00 USD
Pedidos: 5
--- Por Categoria ---
Electronics:
Pedidos: 3
Receita: $7236.00 USD
Média: $2412.00 USD
Clothing:
Pedidos: 1
Receita: $961.20 USD
Média: $961.20 USD
Books:
Pedidos: 1
Receita: $129.60 USD
Média: $129.60 USD
❓ Perguntas Frequentes
P: Qual é a diferença entre Stream e Future? R: Um Future representa um único resultado assíncrono (0 ou 1 valor), enquanto um Stream representa uma sequência de eventos assíncronos (0 a muitos valores). Um Stream é uma generalização de um Future — uma versão de múltiplos valores.
P: Um Stream de subscrição única pode ser escutado múltiplas vezes? R: Não. Um Stream de subscrição única só pode ser escutado uma vez; uma segunda escuta lançará um StateError. Use um Stream broadcast (via
asBroadcastStream()ouStreamController.broadcast()) quando múltiplos ouvintes forem necessários.
P: Operadores de Stream são preguiçosos (lazy)? R: Sim. Operadores como map/where não executam imediatamente; eles só começam a processar quando escutados. Isso é chamado de "stream frio" (cold stream).
P: Como lidar com erros em um Stream? R: Existem três formas: o callback onError no listen, o operador handleError, ou envolver
await forcom try-catch. Usar o callback onError no listen é recomendado.
P: Um StreamController precisa ser fechado? R: Sim. Chame controller.close() para fechar o stream e notificar os ouvintes de que o stream terminou. Não fechá-lo fará com que os ouvintes aguardem indefinidamente.
P: Quais são as limitações de um Stream broadcast? R: Um Stream broadcast não suporta pausa/retomada, e se um ouvinte se inscrever após um evento ter sido emitido, ele perderá os eventos anteriores. É adequado para cenários de "inscrição em tempo real".
P: Um Stream suporta contrapressão (backpressure)? R: Um Stream de subscrição única naturalmente suporta contrapressão — quando o ouvinte (consumidor) processa lentamente, o produtor aguardará. Um Stream broadcast não suporta contrapressão.
📖 Resumo
- Um Stream é uma sequência de eventos assíncronos, dividido em subscrição única (1 ouvinte) e broadcast (múltiplos ouvintes)
- Métodos de criação: fromIterable (dados existentes), periodic (geração temporizada), StreamController (controle manual)
- Cadeia de operadores: where filtrar → map transformar → expand achatar → take/skip limitar → distinct deduplicar
- StreamController é a ponta de produção de um Stream, enquanto StreamSubscription é a ponta de consumo
- Um pipeline de Stream permite o processamento de milhões de dados um a um, com uso de memória estável e suporte a processamento em tempo real
📝 Exercícios
- Básico (Dificuldade ⭐): Use
Stream.fromIterablepara criar um stream contendo 10 valores, usewherepara filtrar aqueles maiores que 500, usemappara convertê-los para formato USD, e uselistenpara imprimir os resultados. - Intermediário (Dificuldade ⭐⭐): Use
StreamControllerpara simular um stream de pedidos em tempo real, adicionando 1 pedido por segundo durante 5 segundos. Construa um pipeline para filtrar pedidos concluídos, agregar receita por categoria e, por fim, gerar um relatório. - Desafio (Dificuldade ⭐⭐⭐): Implemente uma função de "mesclar streams" que combine dois streams de pedidos em um (intercalados por tempo), deduplicando-os e depois processando-os. Dica: Use
StreamGroup(do pacoteasync) ou mescle manualmente comStreamController.