Olá! Meu nome é Gildásio. Sou engenheiro de software da equipe do iFood para Parceiros e esta é a segunda parte de uma série sobre como estamos reescrevendo, refatorando e redesenhando nosso aplicativo na preparação de uma nova grande funcionalidade que o tornará um superaplicativo Flutter.
A versão original deste artigo foi publicada em inglês, dá uma olhada lá!
A primeira parte foi sobre como lidamos com a migração para o Flutter 2.0, enquanto planejávamos uma nova arquitetura para nossa camada de interface. Agora, vou entrar em mais detalhes sobre o que tínhamos antes, o que acabamos fazendo e como estamos depois de trabalhar com essa nova arquitetura.
Mas o que é um módulo?!
Em primeiro lugar, provavelmente devo explicar o que é esse negócio que venho chamando de “módulo”. Um módulo em nosso aplicativo é um pacote Dart separado que tem todas as camadas necessárias divididas da seguinte forma:
- domain — entidades, casos de uso e abstrações de repositórios;
- data — implementações dessas abstrações;
- ui — páginas, views e widgets; com blocs dentro da pasta de cada página.
Desta forma, podemos gerenciar as dependências de cada pacote que não precisam ser usadas em outras partes do aplicativo, e controlar o que exatamente sai de cada pacote caso precise usar em outro.
Este é um dos módulos do aplicativo, lá embaixo tem um exemplo usando ele!
Nesse artigo vamos falar sobre a terceira camada, a interface do usuário, e sobre como estamos integrando um novo gerenciamento de estado enquanto pensamos numa versão própria para torná-lo ainda mais fácil de usar.
O que tínhamos
Conforme explicado antes, o aplicativo tinha dois anos de código baseado em uma arquitetura que dificultou um pouco nossa migração para o Flutter 2.0 e, embora as camadas de domain e data bem organizados, a UI era uma mistura de Provider + MVP + rxdart e não estava muito amigável de trabalhar para alguém que acabou de chegar na equipe. Vindo de dois projetos consecutivos utilizando o bloc, sugeri e decidi com a equipe como poderíamos facilitar ainda mais o trabalho, sabendo que os novos membros ainda não tinham nenhuma experiência com ela.
O Business Logic Component (componente de regra de negócio) pode ser difícil de compreender vindo de outros tipos de arquitetura, como MVC/MVP e até mesmo MVVM, que considero ser a mais próxima, então pensamos “vamos usar a versão mais básica e reiterar ao longo do tempo”. Portanto, embora estejamos realmente usando a biblioteca bloc, não estamos usando a classe Bloc ainda. Estamos usando Cubits!
Optamos pela simplicidade em apenas ter funções que alteram o estado ao invés de depender de eventos, o que poderia ser mais um passo para os novos membros tentarem entender, além de ganhar mais código para as classes de eventos. Perdemos a rastreabilidade de um bloc normal, mas enquanto estivermos monitorando manualmente cada transição de estado, não devemos ter tanto problema. Para os nomes de cada componente / classe / arquivo, entretanto, decidimos chamá-los de Blocs no caso de precisarmos “evoluir” para uma página mais complexa. De agora em diante no artigo, quando digo “bloc”, estou me referindo a um Cubit (o que é verdade, pois ambos vem do BlocBase).
O que fizemos
Com os blocs em mãos, tivemos que planejar como exatamente os usaríamos; que tipo de estados, tratamento de erros e gerenciamento de widgets fariam parte dessa nova arquitetura.
Para os estados, decidimos manter tudo simples, tendo Loading, Failure e Success/Loaded para cada bloco; e às vezes, tendo a variante Initial para páginas que precisam de qualquer tipo de input primeiro. Portanto, qualquer novo bloc teria 3 estados básicos e ficaria livre para ser atualizado conforme necessário.
Para tratamento de erros, decidimos manter os casos de uso “livres” para lançar exceções que seriam capturadas pelo bloc e, em seguida, convertidas em estados de Failure, que então mostrariam um Snackbar ou uma visualização total com base na situação. Para mensagens de erro baseadas no que aconteceu, já que estamos separando cada ação em uma função diferente, então já temos como diferenciar cada mensagem. Basta ter um enum com todos os tipos de erro para cada bloc, e aí adicionar esse enum ao estado de Failure e, no BlocListener, trocar o enum por uma String localizada(estamos usando i18n!), já que decidimos não ter nenhum código Flutter dentro dos blocs.
Com o feedback do nosso tech lead, que tem mais de 8 anos de experiência em desenvolvimento Android, criamos uma hierarquia de três camadas para a parte de gerenciamento da tela: páginas, views e widgets.
- Uma página representa qualquer Widget que tenha uma rota de navegação para ele; geralmente essas são as classes de widget que tem um Scaffold e terão um título de página separado em sua AppBar. Uma página só pode mostrar uma view por vez;
- Uma view representa qualquer relacionamento Estado-Widget; um estado Loaded tem uma view para “renderizá-lo”, um Failure também pode ter uma, e o Loading é apenas nosso indicador de carregamento personalizado, mas estamos livres para transformá-lo em outra coisa. E então temos uma view “vazia” baseada no resultado do Loaded, se for uma lista, então apenas mudamos a visão para a variante Empty. Uma view pode ter quantos widgets forem necessários.
- E aí um widget é o componente mais baixo de uma página ou view e é simplesmente qualquer coisa complexa o suficiente para ganhar sua própria classe/arquivo separado como um componente. Devem estar livres de dependências, receber tudo o que precisam em seu construtor e quem está o usando deve cuidar da integração com seu bloc.
Se tudo isso parece semelhante ao que você veria em um aplicativo Android, é porque é! É basicamente o esquema Activity-Fragment-View, mas para Flutter e com alguns nomes trocados. Não que isso seja algo complicado: só criamos uma etapa adicional para vincular um estado a algo que o constrói como um Widget e chamamos de view.
Exemplos
Os três estados da nossa tela de cardápio na nova arquitetura
Agora com um exemplo, vejamos a ChooseMenuPage acima, que usa o ChooseMenuBloc.
Assim que você entra nessa página, o estado inicial do bloc é o ChooseMenuLoadingState, que faz renderizar o que chamamos de PomodoroLoading. Em seguida, a função bloc.getMenus() é chamada, e como já estamos no Loading, tudo que ela faz é chamar o caso de uso responsável por buscar a lista de cardápios, e redirecionar esse resultado no ChooseMenuLoadedState, que vira a ChooseMenuLoadedView, na imagem do meio.
Se qualquer coisa der errado dentro da função, o ChooseMenuFailureState aparece para salvar o dia, renderizando uma mensagem de erro e um botão para tentar novamente, que vai levar a tela para o ChooseMenuLoadingState e reiniciar o ciclo.
E como estamos?
Se você estava esperando alguma conclusão maior sobre como gerenciar suas telas, infelizmente este não é o seu artigo! Decidimos em conjunto sobre qual seria a melhor maneira de atualizar a infraestrutura de comunicação entre repositórios e telas para que novos membros da equipe pudessem ter tudo do jeito mais fácil possível em um novo módulo.
Estamos bem felizes até agora, ter vinculado o estado a uma view facilitou a nossa vida e diminuiu o tamanho das nossas páginas, que agora só se preocupam em saber quais views usar, e quando. Cada view pode ser tão complexa quanto quiser já que está em outro arquivo, e podemos testá-las separadamente também. Esperamos que esse artigo tenha ajudado a pensar em algum outro jeito de organizar sua camada de interface em Flutter, e sintam-se livres para discutir sobre nossa solução nos comentários!
A próxima, e última, parte dessa série de artigos vai falar sobre nossa jornada em integrar o atual Gestor de Pedidos, que só existe para Android hoje, dentro do nosso superaplicativo do iFood para Parceiros.