Recentemente, tive que implementar orientações de integração para os novatos no ClickUp! Essa foi uma tarefa muito importante, pois muitos novos usuários estavam prestes a descobrir a plataforma com o anúncio incrivelmente engraçado que estreamos no Super Bowl! ✨
via ClickUp
O passo a passo permite que nossos inúmeros novos usuários, que talvez ainda não conheçam o ClickUp, entendam rapidamente como usar várias funcionalidades do aplicativo. É um esforço contínuo, assim como o novo recurso ClickUp University que estamos desenvolvendo! 🚀
Felizmente, a arquitetura de software por trás do aplicativo móvel ClickUp Flutter me permitiu implementar essa funcionalidade rapidamente, mesmo reutilizando os widgets reais do aplicativo! Isso significa que o passo a passo é dinâmico, responsivo e corresponde exatamente às telas reais do aplicativo — e continuará assim, mesmo quando os widgets evoluírem.
Também consegui implementar a funcionalidade devido à separação adequada das preocupações.
Vamos ver o que quero dizer aqui. 🤔
Separação de interesses
Projetar uma arquitetura de software é um dos tópicos mais complexos para equipes de engenharia. Entre todas as responsabilidades, é sempre difícil antecipar as evoluções futuras do software. É por isso que criar uma arquitetura bem estruturada e desacoplada pode ajudar você e seus colegas de equipe em muitas coisas!
A principal vantagem de criar pequenos sistemas desacoplados é, sem dúvida, a testabilidade! E foi isso que me ajudou a criar uma alternativa de demonstração das telas existentes do aplicativo!
Um guia passo a passo
Agora, como poderíamos aplicar esses princípios a um aplicativo Flutter?
Compartilharemos algumas técnicas que usamos para construir o ClickUp com um exemplo simples passo a passo.
O exemplo é tão simples que pode não esclarecer todas as vantagens por trás dele, mas acredite, ele ajudará você a criar aplicativos Flutter muito mais fáceis de manter com bases de código complexas. 💡
O aplicativo
Como exemplo, criaremos um aplicativo que exibe a população dos EUA para cada ano.
via ClickUp
Temos duas telas aqui:
- HomeScreen: simplesmente lista todos os anos desde 2000 até agora. Quando os usuários tocam em um bloco de ano, eles navegam para a DetailScreen com um argumento de navegação definido para o ano selecionado.
- DetailScreen: obtém o ano a partir do argumento de navegação, chama a API datausa.io para este ano e analisa os dados JSON para extrair o valor populacional associado. Se os dados estiverem disponíveis, uma etiqueta é exibida com a população.
Vamos nos concentrar na implementação do DetailScreen, pois é a mais interessante com sua chamada assíncrona.
Etapa 1. Abordagem ingênua

A implementação mais óbvia para nosso aplicativo é usar um único StatefulWidget para toda a lógica.
Acessando o argumento de navegação do ano
Para acessar o ano solicitado, lemos o RouteSettings do widget herdado ModalRoute.
Chamada HTTP
Este exemplo invoca a função get do pacote http para obter os dados da API datausa.io, analisa o JSON resultante com o método jsonDecode da biblioteca dart:convert e mantém o Future como parte do estado com uma propriedade chamada _future.
Renderização
Para criar a árvore de widgets, estamos usando um FutureBuilder, que se reconstrói de acordo com o estado atual de nossa chamada assíncrona _future.
Revisão
Ok, a implementação é curta e usa apenas widgets integrados, mas agora pense em nossa intenção inicial: criar alternativas de demonstração (ou testes) para essa tela. É muito difícil controlar o resultado da chamada HTTP para forçar o aplicativo a renderizar em um determinado estado.
É aí que o conceito de inversão de controle nos ajudará. 🧐
Etapa 2. Inversão de controle

Esse princípio pode ser difícil de entender para novos desenvolvedores (e também difícil de explicar), mas a ideia geral é extrair as preocupações fora de nossos componentes — para que eles não sejam responsáveis por escolher o comportamento — e delegá-las.
Em uma situação mais comum, consiste simplesmente em criar abstrações e injetar implementações em nossos componentes para que sua implementação possa ser alterada posteriormente, se necessário.
Mas não se preocupe, tudo ficará mais claro após o nosso próximo exemplo! 👀
Criando um objeto cliente API
Para controlar a chamada HTTP para nossa API, isolamos nossa implementação em uma classe DataUsaApiClient dedicada. Também criamos uma classe Measure para facilitar a manipulação e manutenção dos dados.
Forneça um cliente API
Para o nosso exemplo, estamos usando o conhecido pacote provider para injetar uma instância DataUsaApiClient na raiz da nossa árvore.
Usando o cliente API
O provedor permite que qualquer widget descendente (como nosso DetailScreen) leia o DataUsaApiClient superior mais próximo na árvore. Podemos então usar seu método getMeasure para iniciar nosso Future, no lugar da implementação HTTP real.
Cliente API de demonstração
Agora podemos aproveitar isso!
Caso você não saiba: todas as classes em dart também definem implicitamente uma interface associada. Isso nos permite fornecer uma implementação alternativa do DataUsaApiClient, que sempre retorna a mesma instância a partir de suas chamadas do método getMeasure.
Este método
Este método
Exibindo uma página de demonstração
Agora temos todas as chaves para exibir uma instância de demonstração da DetailPage!
Simplesmente substituímos a instância DataUsaApiClient fornecida atualmente, envolvendo nosso DetailScreen em um provedor que cria uma instância DemoDataUsaApiClient!
E é isso: nosso DetailScreen lê essa instância de demonstração e usa nossos dados demoMeasure em vez de uma chamada HTTP.
Revisão
Este é um ótimo exemplo de Inversão de controle. Nosso widget DetailScreen não é mais responsável pela lógica de obtenção dos dados, mas delega essa tarefa a um objeto cliente dedicado. E agora podemos criar instâncias de demonstração da tela ou implementar testes de widget para nossa tela! Incrível! 👏
Mas podemos fazer ainda melhor!
Como não podemos simular um estado de carregamento, por exemplo, não temos controle total sobre nenhuma mudança de estado em nosso nível de widget.
Etapa 3. Gerenciamento de estado

Este é um tema muito discutido no Flutter!
Tenho certeza de que você já leu longas discussões de pessoas que tentam eleger a melhor solução de gerenciamento de estado para o Flutter. E, para ser claro, não é isso que faremos neste artigo. Em nossa opinião, desde que você separe sua lógica de negócios da lógica visual, tudo ficará bem! Criar essas camadas é muito importante para a manutenção. Nosso exemplo é simples, mas em aplicativos reais, a lógica pode se tornar complexa rapidamente e essa separação torna muito mais fácil encontrar seus algoritmos de lógica pura. Esse assunto é frequentemente resumido como gerenciamento de estado.
Neste exemplo, estamos usando um ValueNotifier básico junto com um Provider. Mas também poderíamos ter usado flutter_bloc ou riverpod (ou outra solução), e teria funcionado muito bem também. Os princípios permanecem os mesmos e, desde que você separe seus estados e sua lógica, é até possível portar sua base de código de uma das outras soluções.
Essa separação também nos ajuda a controlar qualquer estado dos nossos widgets para que possamos simular de todas as maneiras possíveis!
Criando um estado dedicado
Em vez de depender do AsyncSnapshot da estrutura, agora representamos o estado da nossa tela como um objeto DetailState.
Também é importante implementar os métodos hashCode e operador == para tornar nosso objeto comparável por valor. Isso nos permite detectar se duas instâncias devem ser consideradas diferentes.
💡 Os pacotes equatable ou freezed são ótimas opções para ajudá-lo a implementar os métodos hashCode e operador ==!
💡 Os pacotes equatable ou freezed são ótimas opções para ajudá-lo a implementar os métodos hashCode e operador ==!
Nosso estado está sempre associado a um ano, mas também temos quatro estados distintos possíveis em relação ao que queremos mostrar ao usuário:
- NotLoadedDetailState: a atualização dos dados ainda não começou
- LoadingDetailState: os dados estão sendo carregados no momento
- LoadedDetailState: os dados foram carregados com sucesso com uma medida associada.
- NoDataDetailState: os dados foram carregados, mas não há dados disponíveis.
- UnknownErrorDetailState: a operação falhou devido a um erro desconhecido
Esses estados são mais claros do que um AsyncSnapshot, pois representam realmente nossos casos de uso. E, mais uma vez, isso torna nosso código mais fácil de manter!
💡 Recomendamos fortemente os tipos Union do pacote freezed para representar seus estados lógicos! Ele adiciona muitos utilitários, como os métodos copyWith ou map.
💡 Recomendamos fortemente os tipos Union do pacote freezed para representar seus estados lógicos! Ele adiciona muitos utilitários, como os métodos copyWith ou map.
Colocando a lógica em um Notificador
Agora que temos uma representação do nosso estado, precisamos armazenar uma instância dele em algum lugar — esse é o objetivo do DetailNotifier. Ele manterá a instância atual do DetailState em sua propriedade de valor e fornecerá métodos para atualizar o estado.
Fornecemos um estado inicial NotLoadedDetailState e um método de atualização para carregar dados da API e atualizar o valor atual.
Forneça um estado para a visualização
Para instanciar e observar o estado da nossa tela, também contamos com o provedor e seu ChangeNotifierProvider. Esse tipo de provedor procura automaticamente qualquer ChangeListener criado e aciona uma reconstrução do Consumidor cada vez que é notificado de uma alteração (quando nosso valor de notificação é diferente do anterior).
Revisão
Ótimo! A arquitetura do nosso aplicativo está começando a ficar muito boa. Tudo está separado em camadas bem definidas, com funções específicas! 🤗
No entanto, ainda falta um elemento para a testabilidade: queremos definir o DetailState atual para controlar o estado do nosso DetailScreen associado.
Etapa 4. Widget de layout visual dedicado

Na última etapa, atribuímos um pouco de responsabilidade demais ao nosso widget DetailScreen: ele era responsável por instanciar o DetailNotifier. E, como vimos anteriormente, tentamos evitar qualquer responsabilidade lógica na camada de visualização!
Podemos resolver isso facilmente criando outra camada para o nosso widget de tela: dividiremos nosso widget DetailScreen em dois:
- DetailScreen é responsável por configurar as várias dependências da nossa tela a partir do estado atual do aplicativo (navegação, notificadores, estado, serviços, etc.).
- DetailLayout simplesmente converte um DetailState em uma árvore dedicada de widgets.
Ao combinar os dois, seremos capazes de criar instâncias de demonstração/teste do DetailLayout de forma simples, mas tendo o DetailScreen para o caso de uso real em nosso aplicativo.
Layout dedicado
Para obter uma melhor separação de interesses, transferimos tudo do widget Consumer para um widget DetailLayout dedicado. Esse novo widget apenas consome dados e não é responsável por nenhuma instanciação. Ele apenas converte o estado de leitura em uma árvore de widgets específica.
A instância ModalRoute. of call e ChangeNotifierProvider permanece no DetailScreen, e este widget simplesmente retorna o DetailLayout com uma árvore de dependências pré-configurada!
Esta pequena melhoria é específica para o uso do provedor, mas você notará que também adicionamos um ProxyProvider para que qualquer widget descendente possa consumir diretamente um DetailState. Isso facilitará a simulação de dados.
Extraindo widgets como classes dedicadas
Não hesite em extrair uma árvore de widgets para uma classe dedicada! Isso melhorará o desempenho e tornará o código mais fácil de manter.
Em nosso exemplo, criamos um widget de layout visual para cada um dos tipos de estado associados:
Instancias de demonstração
Agora temos controle total sobre o que podemos simular e exibir em nossa tela!
Basta envolver um DetailLayout com um Provider
Conclusão
Criar uma arquitetura de software sustentável definitivamente não é fácil! Antecipar cenários futuros pode exigir muito esforço, mas espero que as dicas que compartilhei ajudem você no futuro!
Os exemplos podem parecer simples — pode até parecer que estamos exagerando na engenharia —, mas à medida que a complexidade do seu aplicativo aumenta, ter esses padrões vai te ajudar muito! 💪
Divirta-se com o Flutter e siga o blog para obter mais artigos técnicos como este! Fique ligado!

