Flutter: além dos widgets

Tempo de leitura: 14 minutes

Flutter é a estrutura portátil do Google para criar aplicativos multiplataforma compilados nativamente a partir de uma única base de código. Além disso, é de código aberto e, portanto, de uso gratuito. Flutter consiste em duas partes essenciais:

  • Um SDK (Software Development Kit): É uma coleção de ferramentas usadas pelos desenvolvedores para criar aplicativos. Ele permite a compilação de código em código de máquina nativo.
  • Uma estrutura baseada em widget (biblioteca de interface do usuário): é um conjunto de elementos de interface do usuário reutilizáveis. Eles permitem que os desenvolvedores criem aplicativos de acordo com suas necessidades específicas.

Em última análise, como Tim Sneath disse durante o Flutter 1.0 Keynote, “Flutter é um kit de ferramentas de interface do usuário poderoso, de uso geral e aberto para criar experiências impressionantes em qualquer dispositivo, incorporado, móvel, desktop ou além”.

No entanto, o desenvolvimento do Flutter App vai muito além de simplesmente entender e alavancar widgets. Portanto, esta postagem do blog fornece uma visão geral dos principais conceitos e elementos com peso crucial na construção de um aplicativo Flutter sustentável e escalável.

 

Arquitetura

A arquitetura de software envolve inúmeras decisões com base em vários fatores, representando o mais alto nível de abstração no qual construímos e projetamos sistemas de software. Em outras palavras, a arquitetura de software define os limites para os níveis de qualidade que os sistemas resultantes podem alcançar e representa uma oportunidade inicial de projetar requisitos de qualidade de software, como reutilização, desempenho, segurança e confiabilidade. No entanto, a arquitetura de um sistema de software raramente é limitada a um único padrão de arquitetura. Assim, proponho empregar uma arquitetura híbrida combinando modularização em camadas e recursos para implementar seu Flutter App.

Em camadas

Uma arquitetura em camadas depende da fragmentação de programas de software em níveis hierárquicos baseados em responsabilidade, chamados de camadas, e é um padrão comum em aplicativos de grande escala. Mais especificamente, recomendo alavancar uma arquitetura em camadas de quatro camadas, incluindo os seguintes níveis distintos:

  • Camada de Dados — É a camada mais baixa de nossa arquitetura de quatro camadas. É responsável por se comunicar com fontes externas (bancos de dados ou APIs) para recuperar dados brutos. Além disso, essa camada expõe os clientes livres de quaisquer dependências específicas da interface do usuário. Por fim, podemos considerar essa camada a camada de engenharia, pois serve ao propósito de processar e transformar dados com eficiência.
  • Camada de repositório — Separa a lógica de negócios e as camadas de dados e compõe um ou mais clientes da camada de dados para aplicar regras de negócios específicas do domínio aos dados recuperados. Repositórios baseados em domínio compõem esta camada cuja principal função é centralizar a funcionalidade de acesso a dados compartilhados, atuando como um intermediário entre a lógica de negócios e as camadas de dados. Além disso, deve haver apenas um repositório por domínio, que deve estar livre de quaisquer dependências do Flutter e só pode interagir com a camada de dados. Por fim, podemos considerar essa camada a camada do produto, pois agrega valor ao usuário por meio da exposição de dados refinados.
  • Camada de lógica de negócios — Embarca a lógica de negócios do aplicativo. Blocos e Cubits compõem um ou mais repositórios e incluem a lógica para apresentar as regras de negócios por meio de um recurso ou caso de uso específico. Além disso, esta camada emprega a biblioteca de blocos para gerenciar a lógica associada a cada recurso. Portanto, podemos considerar esta camada a camada de feição, pois determina o funcionamento correto de qualquer feição. A seção de gerenciamento de estado a seguir fornece informações mais abrangentes sobre essa camada e sua implementação.
  • Camada de Apresentação — É a camada superior da nossa arquitetura de quatro camadas. Ele serve como a interface do usuário do aplicativo, permitindo que os usuários interajam diretamente com ele. Essas interações geram eventos, que são encaminhados para a camada de lógica de negócios. Além disso, ele reage às mudanças de estado da camada de lógica de negócios, solicitando que a IU acione modificações de renderização por meio de Flutter Widgets. Além disso, esta camada deve interagir apenas com a camada de lógica de negócios. Por fim, podemos considerar essa camada a camada de design, pois visa fornecer a melhor experiência possível para os usuários por meio de componentes e efeitos visuais.

 

Orientado a recursos

O número de noções e interpretações de um recurso é tão amplo e abstrato quanto as definições podem ter. Portanto, proponho a seguinte definição ampla e abrangente de conceito fundamental: Um recurso é um aspecto, qualidade ou característica proeminente ou distinta visível ao usuário de um sistema de software ou sistemas que representam uma entidade de funcionalidade que satisfaz um requisito, atende a uma decisão de design , e fornece uma possível opção de configuração. Além disso, o Desenvolvimento de Software Orientado a Funcionalidades (FOSD) é um paradigma para a construção, customização e síntese de sistemas de software de larga escala com o objetivo de decompor tal sistema com base nas funcionalidades que ele fornece. A decomposição permite a criação de sistemas de software bem estruturados e adaptados às necessidades do usuário, ao mesmo tempo em que facilita a reutilização de recursos compartilhados para gerar diferentes sistemas de software. Por fim, vale abordar o conceito de feição por camada:

  • Recurso de infraestrutura — É um recurso encontrado na camada de dados e apresentado como um módulo cliente que adere ao papel dessa camada, comunicando-se com fontes externas e buscando dados. Ele adiciona funcionalidade em um nível baixo dentro da estrutura do aplicativo e, portanto, não fornece valor direto aos usuários do aplicativo, pois eles o percebem como uma caixa preta.
  • Recurso de domínio — É um recurso encontrado na camada de domínio e apresentado como um repositório baseado em domínio que adere à função dessa camada aplicando regras de negócios específicas do domínio aos dados recuperados. Ele adiciona funcionalidade em um nível intermediário dentro da estrutura do aplicativo e, portanto, não fornece valor direto aos usuários do aplicativo, pois eles ainda o percebem como uma caixa preta.
  • Recurso de aplicativo — É um recurso encontrado na camada de lógica de negócios (componente lógico), na camada de apresentação (componente de design) ou em ambas as camadas (componente combinado) que adere à função dessa camada fornecendo funcionalidade ou valor visual ou ambos. Observe que esse recurso fica no nível mais alto da estrutura do aplicativo e, portanto, exibe valor direto para os usuários do aplicativo, pois eles podem interagir com esse recurso.

 

Embalagem

Além das vantagens fornecidas pela combinação dos padrões de arquitetura em camadas e orientados a recursos, considero fundamental complementar nossa arquitetura com uma abordagem de modularização eficaz. Essa abordagem aproveita todo o poder do dart, permitindo a compartimentalização de recursos por camada e recurso. A estrutura do projeto proposto também deve aderir à abordagem Multimodule Monorepo, que permite manter um projeto como um único repositório com vários submódulos para cada um dos recursos incluídos em cada camada.

Arquitetura híbrida com monorepo multimódulo
Arquitetura híbrida com monorepo multimódulo

No final, seguir esses padrões e decisões arquiteturais permite que os desenvolvedores adicionem, modifiquem, removam e testem facilmente recursos e resolvam bugs e correções sem afetar o trabalho de outros colegas de projeto, aprimorando a manutenibilidade geral e a escalabilidade de um determinado aplicativo de software.

 

Gerenciamento de estado

O gerenciamento de estado é indiscutivelmente uma das decisões mais controversas, debatidas e críticas que os arquitetos e engenheiros de software devem tomar ao implementar qualquer aplicativo de software. É também uma das primeiras decisões, influenciando completamente a implementação, particularmente os recursos de aplicativos encontrados nas camadas arquitetônicas superiores. Assim, escolher a solução correta de gerenciamento de estado é uma decisão que envolve ponderar as vantagens e desvantagens oferecidas pelas opções analisadas, ao mesmo tempo em que leva em consideração os problemas não triviais que podem surgir caso a solução escolhida seja alterada durante o processo de desenvolvimento. Assim, esta seção apresenta os critérios de seleção empregados para selecionar BLoC e flutter_bloc como a solução de gerenciamento de estado proposta.

Observe que este critério de seleção não pretende se tornar o modelo definitivo para selecionar uma solução de gerenciamento de estado em quaisquer circunstâncias, mas, em vez disso, permite que qualquer desenvolvedor que queira criar um aplicativo Flutter sustentável, escalável e testável encontre a opção mais adequada. Portanto, a solução de gerenciamento de estado escolhida deve ser previsível, simples e altamente testável e aumentar o conforto e a confiança dos desenvolvedores na construção e manutenção de um produto robusto. Em última análise, a melhor solução de gerenciamento de estado é aquela que funciona melhor para você. Assim, proponho o padrão BLoC e sua implementação Flutter através do pacote flutter_bloc.

 

Previsibilidade

Os desenvolvedores geralmente enfrentam desafios significativos ao determinar com precisão o estado do aplicativo desenvolvido em um determinado momento. O Flutter apresenta a Widget Tree para resolver essa ambiguidade desafiadora, permitindo que os desenvolvedores modifiquem o estado dos Widgets e, portanto, a estrutura da Widget Tree. No entanto, gerenciar estados de widgets vertical e horizontalmente em uma árvore de widgets complexa é uma tarefa tortuosa e intrincada. Portanto, flutter_bloc permite que os desenvolvedores decomponham o estado do aplicativo em máquinas de estado menores, bem definidas e determinísticas para lidar com essa complexidade e ambiguidade. Por fim, essas máquinas de estado transformam eventos em zero, um ou vários estados previsíveis.

Simplicidade

Aviso Os aplicativos são reativos por natureza e, portanto, os desenvolvedores devem programá-los para serem reativos. No entanto, essa reatividade natural é a causa de interações não determinísticas do usuário que podem ocorrer a qualquer momento, se ocorrerem. Portanto, os desenvolvedores tradicionalmente contam com APIs poderosas, mas complexas, para gerenciar essa reatividade e produzir aplicativos interativos e envolventes. Por outro lado, flutter_bloc propõe uma API simplificada que abstrai a complexidade dos fluxos enquanto ainda respeita a reatividade natural dos aplicativos. Assim, os desenvolvedores não precisam manter assinaturas de fluxo não triviais e ciclos de vida, permitindo que eles se concentrem em interações previsíveis, manipulando eventos recebidos e gerando novos estados.

Alta testabilidade

Testabilidade é um recurso crucial de qualquer aplicativo de software de alta qualidade. Além disso, devemos buscar e entregar 100% de cobertura por meio de testes de unidade para aumentar a confiança dos desenvolvedores na entrega de produtos confiáveis e de qualidade. Além disso, a flutter_library faz do teste de código um de seus principais valores e fornece um pacote dedicado para tal finalidade, bloc_test. Este pacote é uma biblioteca de utilitários que elimina a complexidade do teste de código reativo enquanto permite que os desenvolvedores testem seu código de unidade e validem o comportamento do produto a qualquer momento com quase nenhuma configuração necessária.

Teste

Devemos considerar o teste de uma área-chave essencial para fornecer aplicativos de alta qualidade, sustentáveis e escaláveis. Além disso, verificar se todo o código se comporta como pretendido permite reduzir o risco, aumentar a confiança em uma determinada base de código e manter as expectativas e suposições atuais alinhadas. Por fim, um teste bem estruturado sempre produzirá o mesmo resultado para qualquer entrada, garantindo a funcionalidade de longo prazo do código, independentemente da funcionalidade e dos recursos adicionados no futuro. Devemos sempre nos esforçar para obter 100% de cobertura de código para toda a nossa base de código como um padrão de qualidade de código e adequação de teste, aplicando o exercício de cada linha de código pelo menos uma vez. Além disso, integrar esses padrões em uma metodologia de trabalho exigirá que os desenvolvedores criem recursos e testes correspondentes como parte do mesmo esforço de engenharia. Essa abordagem incentiva a propriedade e a responsabilidade do código, aumentando potencialmente a produtividade e os comportamentos previsíveis em uma equipe de engenharia. Por fim, um conjunto de testes abrangente requer tempo extra para escrever e manter, o que pode atrapalhar o progresso inicial das tarefas de desenvolvimento puro. No entanto, à medida que uma base de código cresce, os testes servem para evitar a ambiguidade dos requisitos, comunicar o comportamento pretendido e identificar e corrigir funcionalidades ou bugs indesejados. Assim, investir tempo na escrita de testes juntamente com a implementação de recursos economiza tempo a longo prazo, evitando reescritas de código e ajudando a fornecer produtos mais estáveis e confiáveis. Por fim, o Flutter facilita os seguintes tipos de testes:

Tipo de TesteDefinição (o que Testa)Quando Melhor Usado
Unidade– Uma função/método/variável isoladamente
– É um teste Dart puro, sem dependência do Flutter
– Testar contrato e lógica de negócios
– Feedback rápido para desenvolvedores para melhorar a confiança do código
Widget– Um único componente de interface do usuário
– A saída é a subárvore do widget, não o próprio widget renderizado
– Testando a interface do usuário isoladamente
– Pode incluir um recurso mais amplo, mas precisa ser contido
Integração– Uma experiência de ponta a ponta com dependências simuladas– Para encontrar quebras de lógica de negócios em um cliente
De ponta a ponta– Uma experiência de ponta a ponta com um back-end e/ou hardware real
– É uma ‘caixa preta’
– Tentar validar a experiência completa do usuário final
Dourado– A especificação ‘pixel a pixel’– Uma vez que você tenha testes de widget e a interface do usuário esteja bloqueada

 

Injeção de dependência

Dependency Injection (DI) é uma prática de design que impõe o fornecimento de classes de serviço para outra classe que depende delas. Além disso, afirma tornar o código mais fracamente acoplado, portanto, tornando o código extensível, passível de manutenção e testável. Este padrão possui práticas específicas para injetar os referidos serviços resumidos abaixo:

  • Injeção de construtor — É a forma mais simples do padrão DI. Os desenvolvedores fornecem uma classe como uma dependência para o construtor de uma classe que requer serviços ou funcionalidades da classe injetada. No entanto, essa abordagem pode levar a um cheiro de código típico em que os desenvolvedores injetam várias dependências em uma determinada classe. Assim, a injeção de propriedades surge como uma possível solução de compensação ao injetar mais de três ou quatro dependências.
  • Injeção de propriedade — É semelhante à injeção de construtor. No entanto, essa abordagem injeta dependências por meio da propriedade setter, em vez do construtor. A dependência é opcional, pois a classe que precisa de seus serviços possui um padrão local, permitindo que os usuários injetem diferentes dependências. Essa abordagem também apresenta alguns pontos fracos, como obscurecer a estrutura interna das classes que implementam essa variante de DI ou injetar repetidamente as mesmas dependências em diferentes partes do programa, complicando as tarefas de depuração.
  • Injeção de método e interface — Ambas as formas executam DI passando uma dependência como argumento para um método. Essas abordagens fornecem dependências dinâmicas durante o tempo de execução e, no caso de injeção de interface, obriga a classe implementando sua interface a fornecer a dependência.

No geral, os defensores dos padrões de DI defendem que eles permitem que os desenvolvedores produzam códigos mais fracamente acoplados, o que aumenta a controlabilidade e a observabilidade dos casos de teste e aprimora a extensibilidade e a reutilização do software, afetando positivamente os atributos de qualidade do software, como manutenibilidade e testabilidade.

 

Mocks

Mocks permitem que os programadores usem objetos fictícios como componentes ou serviços falsos em vez de reais para garantir o funcionamento correto dos testes de unidade. Essa prática reforça o princípio fundamental dos testes de unidade, que é testar a menor unidade isoladamente. No entanto, aplicativos de software reais exibem classes que se comunicam com outros componentes e serviços, impedindo o teste de método isolado e quebrando o objetivo original do teste de unidade. Assim, zombar desses componentes e serviços permite aos desenvolvedores desacoplar dependências externas e executar testes unitários de forma isolada, facilitando a construção e execução da suíte de testes de um sistema.

Além disso, mock é um termo geral que abrange uma família de implementações semelhantes para substituir recursos externos reais durante o teste de unidade. Existem outros termos semelhantes, como dummy, stub, fake e mock, causando confusão para desenvolvedores e leitores devido às suas vagas diferenças. Portanto, nós os distinguimos no grupo stub, incluindo dummy, fake e stub, e no grupo fictício, incluindo ele mesmo. Para esclarecer essa separação, observe que os stubs são recursos substitutos que fornecem apenas os dados necessários, enquanto os mocks estendem esse conceito com o comportamento do objeto. Portanto, os desenvolvedores usam stubs para criar testes de verificação de estado e simulações para criar testes que verificam chamadas de método, frequência de chamada e ordem de chamada. Assim, o padrão simulado inclui as cinco etapas a seguir:

  1. Criar objeto fictício
  2. Configurar o estado do objeto fictício
  3. Configurar expectativa de objeto fictício
  4. Forneça um objeto fictício ao código de domínio
  5. Verifique o comportamento no objeto fictício

Observe que esse padrão destaca como as etapas de um a quatro são comuns a stubs e simulações, enquanto a etapa cinco se aplica apenas a simulações, pois inclui verificação de comportamento.

Por fim, os mocks fornecem aos desenvolvedores inúmeros benefícios valiosos, como execução rápida de testes de unidade devido ao desacoplamento de recursos externos, reprodução de erros, localização de testes de unidade, capacidade de controle e observabilidade garantidas e previsibilidade consistente.

 

Dart

Por último, mas não menos importante, tornar-se totalmente proficiente na linguagem Dart terá um efeito tremendamente positivo em como você codifica aplicativos Flutter, pois essa linguagem de programação forma a base do Flutter, fornecendo a linguagem e os tempos de execução para alimentar os aplicativos dessa estrutura. O design 24 de seu envelope técnico é particularmente adequado para o desenvolvimento do cliente, priorizando experiências de desenvolvimento e produção de alta qualidade em uma ampla variedade de destinos de compilação (web, móvel, desktop e incorporado). Além disso, o Dart usa verificação de tipo estático para garantir que o valor de uma variável sempre corresponda ao tipo estático da variável, tornando-a segura para o tipo. Ele também oferece suporte a tipos dinâmicos combinados com verificações de tempo de execução, oferecendo mais flexibilidade ao sistema de digitação do idioma. No geral, o Dart é uma linguagem otimizada para o cliente para desenvolver aplicativos rápidos em qualquer plataforma. Assim, o Dart permite a compilação de código nessas diferentes plataformas:

  • Nativo — Para aplicativos direcionados a dispositivos móveis e de desktop, o Dart inclui uma VM Dart com compilação just-in-time (JIT) e um compilador antecipado (AOT) para produzir código de máquina
  • Web — Para aplicativos voltados para a Web, o Dart inclui um compilador de tempo de desenvolvimento (dartdevc) e um compilador de tempo de produção (dart2js). Ambos os compiladores traduzem o Dart em JavaScript.

 

Sound Null Safety

O suporte de segurança nula do Dart merece atenção especial, pois força as variáveis a serem não anuláveis por padrão, a menos que os desenvolvedores definam de outra forma em seu código. Além disso, a segurança nula transforma erros de deferência nula de tempo de execução em erros de análise de tempo de edição, aprimorando a experiência de desenvolvimento e minimizando exceções e bugs de tempo de execução. Por fim, o suporte de segurança nula do Dart se baseia nos três princípios básicos de design a seguir:

  • Non-nullable por padrão — Variáveis não são anuláveis, a menos que explicitamente declarado de outra forma.
  • Adotável incrementalmente — Os desenvolvedores decidem o que e quando migrar para segurança nula, permitindo migrações incrementais e misturando código seguro com código não seguro.
  • Fully sound – O sound null safety do Dart permite otimizações do compilador, pois os tipos não anuláveis nunca podem se tornar anuláveis.

“Null safety é a maior mudança que fizemos no Dart desde que substituímos o sistema de tipo opcional insalubre original por um sistema de tipo estático sólido no Dart 2.0.”

 

Programação assíncrona

A programação assíncrona é um assunto notavelmente relevante na programação Dart e Flutter. Aproveitar seus recursos poderosos permite arquitetar um aplicativo e seu código de maneira mais organizada e eficiente. Além disso, as classes Future e Stream caracterizam a programação assíncrona no Dart.

  • Future — É o resultado de uma computação assíncrona, que é uma computação que não pode retornar um resultado imediato após ser executada. Assim, este resultado pode ter dois estados, incompleto ou completo. O primeiro refere-se a uma futura espera pela operação assíncrona de uma função terminar ou gerar um erro, enquanto o segundo se refere a uma computação concluída com sucesso ou com falha
  • Stream — Um fluxo é uma sequência de eventos assíncronos, diferenciados como dados ou eventos de erro. Confira estes links para se familiarizar com esta poderosa ferramenta
    Asynchronous programming: Streams
    Reactive Programming — Streams — BLoC

 

Packages

Compreender as partes que compõem entidades maiores e mais complexas permite que os engenheiros de software arquitetem aplicativos aproveitando os princípios de modularidade funcional e comportamental. A modularidade funcional refere-se à composição de componentes independentes menores com limites e funções claras. Por outro lado, a modularidade comportamental aborda traços e atributos que podem evoluir de forma independente. Assim, aplicativos de software complexos podem ser divididos em partes funcionais chamadas módulos, que podem ser criadas, alteradas, testadas, usadas e substituídas separadamente. A modularidade do software traz os seguintes benefícios aos sistemas de software:

  • Módulos mais leves com menos código
  • Introdução de novas funcionalidades ou alterações em módulos de forma isolada, separada dos demais módulos
  • Fácil identificação e correção de erros específicos do módulo
  • Os módulos podem ser construídos e testados independentemente
  • Colaboração aprimorada para desenvolvedores que trabalham em diferentes módulos para o mesmo aplicativo
  • Reutilização de módulos em vários aplicativos
  • Os módulos mantidos em um sistema de controle de versão podem ser facilmente mantidos e testados
  • Correções de módulo e alterações não infraestruturais não afetam outros módulos

O Dart favorece a modularidade e fornece um ecossistema baseado em pacotes para gerenciar software compartilhado, como bibliotecas e ferramentas. No mínimo, um pacote Dart é um diretório contendo um arquivo pubspec, um arquivo baseado em yaml contendo metadados sobre o pacote. Os pacotes de avisos podem conter dependências, bibliotecas, aplicativos, recursos, testes, imagens e exemplos. Além disso, o conceito de separação de preocupações entre objetos na programação orientada a objetos (OOP) se assemelha a uma biblioteca Dart, que expõe a funcionalidade como um conjunto de interfaces e oculta a implementação do resto do mundo. As bibliotecas permitem uma melhor estrutura de aplicativo, minimização de acoplamento rígido e código mais fácil de manter. Por fim, um aplicativo Dart também é uma biblioteca.

 

Observações

Esta postagem de blog forneceu uma abordagem padronizada para criar aplicativos Flutter. Em primeiro lugar, propôs uma arquitetura híbrida combinando os pontos fortes e vantagens dos padrões de design arquitetônico em camadas e orientados a recursos. Além disso, aprimoramos essa arquitetura híbrida complementando-a com uma abordagem de modularização baseada em pacotes Dart. Em relação à solução de gerenciamento de estado selecionada, ela forneceu critérios coerentes para apoiar a escolha do padrão BLoC e sua implementação Flutter com flutter_bloc. Além disso, enfatizou a importância do teste como uma atividade central do ciclo de vida de desenvolvimento de software, ao mesmo tempo em que impõe uma cobertura de código de cem por cento para toda a base de código do aplicativo, defendendo seus efeitos positivos na capacidade de manutenção e escalabilidade de longo prazo de qualquer aplicativo Flutter. Por fim, destacou a necessidade inevitável de se tornar fluente em programação Dart para levar suas habilidades Flutter para o próximo nível ao criar aplicativos.

Não deixe de conhecer meus Ebooks de Flutter/Dart