Módulo 02 — Exercícios: Fundamentos da Linguagem Dart

Estes exercícios foram pensados para consolidar o que você estudou no material do Módulo 02. Eles não são apenas testes de memorização: cada um exige que você raciocine sobre o que a linguagem Dart garante, sobre as escolhas de design que você faria em um projeto real, e sobre as consequências práticas de cada decisão. Leia o enunciado com calma, pense antes de escrever, e use o material do módulo como referência quando necessário — mas tente resolver sozinho antes de consultá-lo.


Exercício 1 — Nível Básico

Modelando o Domínio com Null Safety e Construtores Nomeados

O ponto de partida de qualquer aplicativo Flutter: as classes de domínio

Antes de construir qualquer tela ou chamar qualquer API, um aplicativo Flutter bem arquitetado precisa de classes que representem os conceitos do problema que ele resolve. Você estudou como fazer isso com null safety, construtores nomeados, fromJson, toJson, copyWith, e sobrescrita de == e hashCode. Este exercício coloca tudo isso junto em um contexto diretamente ligado ao seu Projeto Integrador.

Considere que o backend da AWS retorna, para cada usuário cadastrado, um JSON com o seguinte formato:

{
  "id": "usr-4f8a",
  "nome_completo": "Ana Lima",
  "email": "ana.lima@exemplo.com",
  "telefone": "+5511999990000",
  "foto_perfil_url": null,
  "ativo": true,
  "criado_em": "2024-09-15T10:30:00Z"
}

Observe que o campo foto_perfil_url pode ser null — nem todo usuário faz upload de foto — e que o campo criado_em chega como uma String no formato ISO 8601, mas você provavelmente vai querer armazená-lo como DateTime no seu modelo Dart.

Sua tarefa é criar, em Dart, a classe Usuario completa, seguindo rigorosamente todas as boas práticas apresentadas no material do Módulo 02. A classe deve atender a todos os requisitos descritos a seguir.

O construtor principal deve usar parâmetros nomeados, com os campos obrigatórios marcados como required e o campo opcional (foto_perfil_url) tratado de forma adequada pelo sistema de null safety. O construtor fromJson deve converter corretamente o Map<String, dynamic> recebido da API em um objeto Usuario, incluindo a conversão da String de data para DateTime. O método toJson deve produzir um mapa que represente fielmente o objeto, formatando o DateTime de volta para uma String ISO 8601 ao serializar. O método copyWith deve permitir criar versões modificadas do objeto sem alterar o original, tratando corretamente o campo anulável. A sobrescrita de == e hashCode deve usar o campo id como critério de igualdade — dois usuários são o mesmo usuário se têm o mesmo id, independentemente dos demais campos. Por fim, o método toString deve produzir uma representação legível do objeto que facilite o trabalho de debug.

Além da classe em si, escreva uma função main que demonstre o funcionamento completo: crie um Usuario a partir do JSON de exemplo acima, imprima seus campos, crie uma versão atualizada com um novo número de telefone usando copyWith, demonstre a igualdade entre dois objetos com o mesmo id e valores diferentes nos demais campos, e serialize o objeto atualizado de volta para Map com toJson.

Antes de começar a digitar, pense nas seguintes questões: qual deve ser o tipo do campo foto_perfil_url na classe Dart? Que tipo Dart representa melhor uma data e hora com timezone? Como você vai converter a String ISO 8601 para esse tipo? Como o copyWith deve se comportar quando o chamador quer explicitamente apagar a foto de perfil — ou seja, passar null como novo valor para foto_perfil_url?

👉 O que deve er entregue: um arquivo chamado g_ex1.dart, onde g é o nome do seu grupo.


Exercício 2 — Nível Intermediário

Processamento Funcional de Coleções e o Padrão Result

Transformar dados em informação é uma habilidade central do desenvolvedor Flutter

Um aplicativo real não apenas exibe dados brutos: ele filtra, agrupa, ordena e agrega informações para apresentar ao usuário exatamente o que ele precisa. Neste exercício, você vai combinar o processamento funcional de coleções com o padrão Result<T> estudado no módulo, construindo um componente que poderia existir literalmente no seu Projeto Integrador.

Você recebeu a seguinte estrutura de dados representando os pedidos registrados no sistema:

enum StatusPedido { pendente, confirmado, preparando, entregue, cancelado }

class ItemPedido {
  final String nomeProduto;
  final int quantidade;
  final double precoUnitario;
  final String? observacao;

  const ItemPedido({
    required this.nomeProduto,
    required this.quantidade,
    required this.precoUnitario,
    this.observacao,
  });
}

class Pedido {
  final String id;
  final String idUsuario;
  final List<ItemPedido> itens;
  final StatusPedido status;
  final DateTime criadoEm;

  const Pedido({
    required this.id,
    required this.idUsuario,
    required this.itens,
    required this.status,
    required this.criadoEm,
  });
}

Sua tarefa é implementar a classe RelatorioPedidos, que recebe uma List<Pedido> e expõe uma série de consultas sobre esses dados. Cada método deve ser implementado com as ferramentas funcionais do Dart — map, where, fold, reduce, any, every e similares — sem usar laços for imperativos. A única exceção é o método de agrupamento, onde você pode usar um laço se julgar necessário.

Os métodos que você deve implementar são os seguintes. O método totalGeral deve retornar a soma dos valores de todos os pedidos não cancelados, onde o valor de um pedido é a soma de precoUnitario * quantidade de todos os seus itens. O método pedidosPorStatus deve retornar um Map<StatusPedido, List<Pedido>> agrupando os pedidos pelo status. O método ticketMedio deve retornar o valor médio de um pedido não cancelado, retornando 0.0 se não houver pedidos válidos para evitar divisão por zero. O método produtoMaisVendido deve retornar o nome do produto que aparece com maior quantidade total somada entre todos os itens de todos os pedidos — considere a quantidade, não o número de ocorrências. O método pedidosDoUsuario deve receber um idUsuario e retornar a lista de pedidos daquele usuário, ordenada do mais recente para o mais antigo. O método contemPedidoUrgente deve retornar true se houver pelo menos um pedido com status pendente criado há mais de 30 minutos em relação ao momento atual.

Após implementar a classe, encapsule a operação de carregar os dados — que em um app real viria de uma chamada assíncrona — em uma função Future<Result<RelatorioPedidos>> gerarRelatorio(List<Pedido> pedidos), usando o padrão sealed class Result<T> com Success e Failure apresentado no material. A função deve retornar Failure se a lista de pedidos estiver vazia, e Success com o relatório caso contrário.

Finalmente, escreva uma função main assíncrona que crie uma lista de pedidos de exemplo (com pelo menos oito pedidos em diferentes status e de diferentes usuários), chame gerarRelatorio, e use switch para tratar os dois casos do Result, imprimindo os dados do relatório no caso de sucesso.

O método produtoMaisVendido é o mais complexo. Pense no problema em etapas: como você transforma uma List<Pedido> em uma lista plana de todos os ItemPedido? Como você agrega as quantidades por nome de produto? Como você encontra a entrada com o maior valor total?

👉 O que deve ser entregue: um arquivo chamado g_ex2.dart, onde g é o nome do seu grupo.


Exercício 3 — Nível Desafiador

Assincronismo, Streams e Extension Methods: Um Serviço de Sincronização

O desafio que une todos os conceitos do módulo

Aplicativos móveis frequentemente precisam sincronizar dados com o servidor, lidar com falhas de rede, emitir progresso para a interface durante operações longas e recuperar graciosamente de erros. Este exercício simula exatamente esse cenário, exigindo que você componha todos os conceitos estudados no módulo em uma solução coerente e bem estruturada.

Você vai implementar um SincronizacaoService que simula o processo de sincronização de um catálogo de produtos com o servidor. O serviço precisa atender a um conjunto preciso de requisitos técnicos e comportamentais.

A estrutura de dados do progresso. Crie uma classe imutável ProgressoSincronizacao com os campos total (int), processados (int), falhas (int) e mensagem (String). Implemente copyWith para permitir atualizações incrementais sem mutação. O percentual de conclusão deve ser exposto como um getter calculado que retorna 0.0 quando total é zero e processados / total * 100 caso contrário, arredondado para uma casa decimal.

O serviço de sincronização. A classe SincronizacaoService deve expor um Stream<ProgressoSincronizacao> chamado progressoStream e um método Future<Result<int>> sincronizar(List<Produto> produtos). O método sincronizar deve: emitir um estado inicial pelo stream indicando que a sincronização começou; processar cada produto individualmente — simulando a latência com Future.delayed de duração aleatória entre 100 e 500 milissegundos; emitir uma atualização de progresso após cada produto processado, incrementando processados para os bem-sucedidos e falhas para os que falharam; simular falha em aproximadamente 20% dos produtos (use Random de dart:math para decidir aleatoriamente); ao final, fechar o stream e retornar Success<int> com o número de produtos sincronizados com sucesso, ou Failure se nenhum produto foi sincronizado com sucesso. Garanta que dispose feche corretamente o StreamController para evitar vazamento de memória.

Os extension methods. Crie uma extensão sobre List<Produto> chamada SincronizacaoProdutoExtension que adicione dois métodos. O primeiro, validos, deve retornar apenas os produtos com preço maior que zero, nome não vazio e marcados como disponíveis. O segundo, agrupadosParaSincronizacao, deve retornar um Map<String, List<Produto>> agrupando os produtos por categoria, ordenando os produtos dentro de cada categoria por preço crescente.

A função principal. Escreva uma função main assíncrona que: crie uma lista de pelo menos dez produtos com variações de categoria, preço e disponibilidade (incluindo alguns inválidos); use o extension method validos para filtrar a lista antes de sincronizar; instancie o SincronizacaoService e inicie a escuta do progressoStream com listen, imprimindo cada atualização formatada como "[XX.X%] N/T processados, F falhas — mensagem"; chame sincronizar com a lista filtrada e aguarde o resultado com await; trate o Result retornado com switch, imprimindo uma mensagem de conclusão adequada para cada caso; e chame dispose ao final para garantir a limpeza dos recursos.

Há um detalhe técnico delicado neste exercício: a escuta ao progressoStream começa antes de sincronizar ser chamado, e os eventos do stream chegam enquanto você está aguardando o Future de sincronizar. Pense cuidadosamente sobre a ordem das operações no main para garantir que nenhum evento seja perdido antes que o listen seja registrado.

👉 O que deve ser entregue: um arquivo chamado g_ex3.dart, onde g é o nome do seu grupo.

Se você conseguiu resolver os três exercícios com sucesso, considere este desafio adicional para o exercício 3: como você tornaria o SincronizacaoService reutilizável — ou seja, capaz de ser chamado múltiplas vezes sem precisar recriar a instância? Que mudança na implementação do StreamController seria necessária para isso?