Guia para a arquitetura do app

Este guia aborda as práticas e a arquitetura recomendadas para a criação de apps robustos com alta qualidade de produção.

Experiências dos usuários de apps para dispositivos móveis

Em geral, um app Android contém vários componentes de app, incluindo atividades, fragmentos, serviços, provedores de conteúdo e broadcast receivers. A maioria desses componentes de app é declarada no manifesto do app. O SO Android usa esse arquivo para decidir como integrar o app à experiência geral do usuário do dispositivo. Como um app Android comum pode conter vários componentes e os usuários geralmente interagem com vários apps em um curto período, os apps precisam se adaptar a diferentes tipos de fluxos de trabalho e tarefas direcionados ao usuário.

Os recursos de dispositivos móveis são limitados, então o sistema operacional pode interromper alguns processos de apps a qualquer momento para dar espaço a outros novos.

Considerando as condições desse ambiente, é possível que os componentes do app sejam iniciados individualmente e fora de ordem, e eles podem ser destruídos a qualquer momento pelo usuário ou pelo sistema operacional. Como esses eventos não estão sob seu controle, não armazene nem mantenha na memória dados ou estados de apps para os componentes do seu app, e não permita que os componentes dele dependam uns dos outros.

Princípios de arquitetura comuns

Se não é recomendável usar componentes do app para armazenar dados e estados, qual é a melhor forma de criar um app?

Conforme os apps para Android aumentam de tamanho, é importante definir uma arquitetura que permita o escalonamento, aumente a robustez e facilite o teste do app.

Uma arquitetura de app define os limites entre as partes do app e as responsabilidades de cada uma. A fim de atender às necessidades mencionadas acima, crie a arquitetura do app para seguir alguns princípios específicos.

Separação de conceitos

O princípio mais importante que precisa ser seguido é a separação de conceitos (link em inglês). É um erro comum escrever todo o código em uma Activity ou um Fragment. Essas classes baseadas em interface precisam conter apenas a lógica que processa as interações entre a interface e o sistema operacional. Ao manter essas classes o mais enxutas possível, você pode evitar muitos problemas relacionados ao ciclo de vida de componentes e melhorar a capacidade de teste dessas classes.

Vale lembrar que a propriedade de implementações da Activity e do Fragment não é sua. Na verdade, elas são apenas classes que representam o contrato entre o SO Android e o app. O SO pode destruí-las a qualquer momento com base nas interações do usuário ou devido a condições do sistema, como pouca memória. Para fornecer uma experiência do usuário satisfatória e uma experiência de manutenção de app mais gerenciável, o melhor é minimizar sua dependência dessas classes.

interface do Drive com base em modelos de dados

Outro princípio importante é que você precisa basear sua interface em modelos de dados, de preferência, modelos persistentes. Os modelos de dados representam os dados de um app. Eles são independentes dos elementos da interface e outros componentes do app. Isso significa que eles não estão vinculados ao ciclo de vida do componente da interface e do app, mas ainda vão ser destruídos quando o SO decidir remover o processo do app da memória.

Os modelos persistentes são ideais por estes motivos:

  • Seus usuários não perderão dados se o SO Android destruir o app para liberar recursos.

  • O app continuará a funcionar caso uma conexão de rede esteja instável ou indisponível.

Se você basear a arquitetura do app em classes de modelo de dados, ele vai se tornar mais testável e robusto.

Única fonte de informações

Quando um novo tipo de dado é definido no seu app, você precisa atribuir uma Única fonte de informações (SSOT, na sigla em inglês) a ele. A SSOT é a proprietária desses dados, e apenas ela pode fazer mudanças neles. Para isso, ela expõe os dados usando um tipo imutável, e para fazer mudanças ela expõe funções ou recebe eventos que outros tipos podem chamar.

Esse padrão traz vários benefícios:

  • Ele centraliza todas as mudanças de um tipo específico de dados em um só lugar.
  • Ele protege os dados para que outros tipos não possam fazer adulterações neles.
  • Ele faz com que as mudanças nos dados sejam mais rastreáveis. Assim, os bugs são mais fáceis de detectar.

Em um aplicativo que prioriza o modo off-line, a fonte da verdade para os dados do aplicativo geralmente é um banco de dados. Em alguns outros casos, ela pode ser um ViewModel ou até mesmo a interface.

Fluxo de dados unidirecional

O princípio da Única fonte de informações geralmente é usado nos nossos guias com o padrão Fluxo de dados unidirecional (UDF, na sigla em inglês). No UDF, o estado flui em apenas uma direção. São os eventos que modificam o fluxo de dados na direção oposta.

No Android, o estado ou os dados geralmente fluem dos tipos de escopo mais altos da hierarquia para os mais baixos. Os eventos geralmente são acionados pelos tipos de escopo mais baixos até alcançarem a SSOT para o tipo de dados correspondente. Por exemplo, os dados do app geralmente fluem das fontes de dados para a interface. Já os eventos do usuário, como pressionamento de botões, fluem da interface para a SSOT, em que os dados do aplicativo são modificados e expostos em um tipo imutável.

Esse padrão garante melhor a consistência dos dados, é menos propenso a erros, é mais fácil de depurar e traz todos os benefícios do padrão SSOT.

Esta seção demonstra como estruturar o app seguindo as práticas recomendadas.

Considerando os princípios de arquitetura comuns mencionados na seção anterior, cada aplicativo precisa ter pelo menos duas camadas:

  • A camada de IU que mostra os dados do aplicativo na tela.
  • A camada de dados que contém a lógica de negócios do app e expõe os dados do aplicativo.

É possível adicionar uma camada extra conhecida como camada de domínios para simplificar e reutilizar as interações entre a IU e as camadas de dados.

Em uma arquitetura de app típica, a camada de IU recebe os dados do aplicativo
    da camada de dados ou da camada de domínios opcional, que fica entre
    a camada de IU e a camada de dados.
Figura 1. Diagrama de uma arquitetura típica de app.

Arquitetura moderna de apps

Esta Arquitetura moderna de apps incentiva o uso das seguintes técnicas, entre outras:

  • Uma arquitetura reativa e em camadas.
  • Fluxo de dados unidirecional (UDF, na sigla em inglês) em todas as camadas do app.
  • Uma camada da interface com detentores de estado para gerenciar a complexidade dela.
  • Corrotinas e fluxos.
  • Práticas recomendadas para injeção de dependência.

Para mais informações, consulte as seções a seguir, as outras páginas de arquitetura no índice e a página de recomendações, que contém um resumo das práticas mais importantes.

Camada de interface

A função da camada de IU (ou camada de apresentação) é exibir os dados do aplicativo na tela. Sempre que os dados mudam, seja devido à interação do usuário, como o pressionamento de um botão, ou a uma entrada externa, como uma resposta de rede, a interface é atualizada para refletir as mudanças.

A camada de IU é composta por dois itens:

  • Elementos da IU que renderizam os dados na tela. Esses elementos são criados usando funções de visualizações ou do Jetpack Compose.
  • Holders de estado, como classes ViewModel, que armazenam dados, os expõem à IU e processam a lógica.
Em uma arquitetura típica, os elementos da interface da camada de interface dependem dos detentores
    de estado, que, por sua vez, dependem de classes da camada de dados ou
    da camada de domínios opcional.
Figura 2. O papel da camada de IU na arquitetura do app.

Para saber mais sobre essa camada, consulte a página sobre a camada de IU.

Camada de dados

A camada de dados de um app contém a lógica de negócios. A lógica de negócios é o que agrega valor ao app. Ela é composta por regras que determinam como o app cria, armazena e muda dados.

A camada de dados é composta por repositórios que podem conter de zero a muitas fontes de dados. Crie uma classe de repositório para cada tipo diferente de dados processados no seu app. Por exemplo, você pode criar uma classe MoviesRepository para dados relacionados a filmes ou uma classe PaymentsRepository para dados relacionados a pagamentos.

Em uma arquitetura típica, os repositórios da camada de dados fornecem dados
    ao restante do app e dependem das fontes de dados.
Figura 3. O papel da camada de dados na arquitetura do app.

As classes de repositório são responsáveis por estas tarefas:

  • Expor dados ao restante do app.
  • Centralizar mudanças nos dados.
  • Resolver conflitos entre várias fontes de dados.
  • Abstrair fontes de dados do restante do app.
  • Conter uma lógica de negócios.

Cada classe de origem de dados deve ser responsável por trabalhar com apenas uma origem, que pode ser um arquivo, uma rede ou um banco de dados local. As classes de fonte de dados são a ponte entre o aplicativo e o sistema para operações de dados.

Para saber mais sobre essa camada, consulte a página sobre a camadas de dados.

Camada de domínios

A camada de domínios é opcional e fica entre a interface e as camadas de dados.

A camada de domínios é responsável por encapsular a lógica de negócios complexa ou simples que é reutilizada por vários ViewModels. Essa camada é opcional, porque nem todos os apps vão ter esses requisitos. Use-a apenas quando necessário, por exemplo, para lidar com a complexidade ou favorecer a reutilização.

Quando incluída, a camada de domínios opcional oferece dependências para
    a camada da IU e depende da camada de dados.
Figura 4. O papel da camada de domínios na arquitetura do app.

As classes nessa camada normalmente são chamadas de casos de uso ou interagentes. Cada caso de uso precisa ser responsável por uma única funcionalidade. Por exemplo, o app pode ter uma classe GetTimeZoneUseCase se vários ViewModels dependerem de fusos horários para mostrar a mensagem adequada na tela.

Para saber mais sobre essa camada, consulte a página da camada de domínios.

Gerenciar dependências entre componentes

As classes no app dependem de outras para funcionar corretamente. É possível usar um dos padrões de design abaixo para reunir as dependências de uma classe específica:

Esses padrões permitem dimensionar o código, porque fornecem padrões claros para gerenciar dependências sem duplicar o código ou elevar a complexidade dele. Além disso, permitem alternar rapidamente entre implementações de teste e de produção.

Recomendamos seguir os padrões de injeção de dependência e usar a biblioteca Hilt em apps Android. A biblioteca Hilt constrói os objetos automaticamente percorrendo a árvore de dependências, além de oferecer garantias de tempo de compilação nas dependências e criar contêineres de dependência para classes do framework do Android.

Práticas recomendadas gerais

A programação é um campo criativo, e a criação de apps Android não é uma exceção. Há muitas maneiras de resolver um problema: é possível comunicar dados entre várias atividades ou fragmentos, extrair dados remotos e os armazenar localmente no modo off-line ou lidar com qualquer outro cenário comum que apps não triviais encontrem.

Embora as recomendações abaixo não sejam obrigatórias, na maioria dos casos a observação delas torna sua base de código mais robusta, testável e de fácil manutenção a longo prazo:

Não armazene dados em componentes do app.

Evite designar os pontos de entrada do app, como atividades, serviços e broadcast receivers, como fontes de dados. Em vez disso, eles precisam se coordenar com outros componentes apenas para extrair o subconjunto de dados relevante para esse ponto de entrada. Cada componente do app tem vida curta, dependendo da interação do usuário com o dispositivo e da integridade geral do sistema.

Reduza as dependências nas classes do Android.

Os componentes do app precisam ser as únicas classes que dependem das APIs do SDK do framework do Android, como Context ou Toast. Abstrair outras classes delas no app melhora a capacidade de teste e reduz o acoplamento no app.

Crie limites de responsabilidade bem definidos entre os vários módulos do app.

Por exemplo, não divulgue o código que carrega dados da rede em várias classes ou pacotes na sua base de código. Da mesma forma, não defina várias responsabilidades não relacionadas, como armazenamento de dados em cache e vinculação de dados, na mesma classe. Seguir a arquitetura de apps recomendada vai ajudar com isso.

Exponha o mínimo possível de cada módulo.

Por exemplo, não crie um atalho que exponha um detalhe de implementação interna de um módulo. Você pode economizar um pouco de tempo a curto prazo, mas provavelmente vai pagar caro por isso tecnicamente à medida que sua base do código progredir.

Concentre-se no núcleo exclusivo do seu app para que ele se destaque de outros.

Não reinvente a roda escrevendo o mesmo código boilerplate várias vezes. Em vez disso, concentre seu tempo e energia no que torna seu app único e deixe que as bibliotecas do Jetpack e outras bibliotecas recomendadas processem o boilerplate repetitivo.

Considere como tornar cada parte do app testável de forma isolada.

Por exemplo, ter uma API bem definida para buscar dados da rede facilita os testes do módulo que mantém esses dados em um banco de dados local. Se, em vez disso, você mesclar a lógica desses dois módulos em um só lugar ou distribuir o código de rede por toda a base de código, vai ser muito mais difícil, ou até impossível, testá-los.

Os tipos são responsáveis pela própria política de simultaneidade.

Se um tipo estiver executando um trabalho de bloqueio de longa duração, ele precisará ser responsável por mover esse cálculo para a linha de execução correta. Esse tipo específico sabe o tipo de cálculo que está sendo feito e em qual linha de execução ele precisa ser executado. Os tipos precisam ser protegidos, ou seja, eles podem ser chamados com segurança da linha de execução principal sem que ela seja bloqueada.

Aplique o máximo de persistência possível em dados relevantes e atualizados.

Dessa forma, os usuários podem aproveitar a funcionalidade do app mesmo quando o dispositivo estiver no modo off-line. Lembre-se de que nem todos os usuários têm conectividade constante e de alta velocidade e, mesmo se tiverem, eles podem ter sinal ruim em alguns lugares lotados.

Benefícios da arquitetura

Ter uma boa arquitetura implementada no app oferece muitos benefícios para as equipes de projetos e engenharia:

  • Melhor manutenção, qualidade e robustez para o app em geral.
  • Possibilidade de escalonamento do app. Mais pessoas e equipes podem contribuir para a mesma base de código com conflitos mínimos.
  • Ajuda na integração. Como a arquitetura traz consistência ao projeto, os novos membros da equipe podem começar a trabalhar mais rápido e ser mais eficientes em menos tempo.
  • Mais fácil de testar. Uma boa arquitetura incentiva tipos mais simples, que geralmente são mais fáceis de testar.
  • Os bugs podem ser investigados metodicamente com processos bem definidos.

Investir na arquitetura também tem um impacto direto nos usuários. Eles se beneficiam de um aplicativo mais estável e com mais recursos graças a uma equipe de engenharia mais produtiva. No entanto, a arquitetura também exige um investimento inicial de tempo. Para justificar esse tempo para sua empresa, confira estes estudos de caso em que outras empresas compartilham as histórias de sucesso delas sobre as vantagens de ter uma boa arquitetura no app.

Exemplos

Os exemplos do Google abaixo demonstram uma boa arquitetura de apps. Acesse-os para ver a orientação na prática: