Não existe uma única estratégia de modularização adequada para todos os projetos. Devido à natureza flexível do Gradle, há poucas restrições para a organização de um projeto. Esta página apresenta um panorama de algumas regras gerais e padrões comuns que você pode empregar ao desenvolver apps Android multimódulo.
Princípio de coesão alta e acoplamento baixo
Uma maneira de caracterizar uma base de código modular seria usando as propriedades de acoplamento e coesão. O acoplamento mede o grau de dependência dos módulos entre si. Nesse contexto, a coesão mede como os elementos de um único módulo são funcionalmente relacionados. Como regra geral, você precisa se esforçar para ter um acoplamento baixo e uma coesão alta:
- O acoplamento baixo significa que os módulos precisam ter o máximo de independência possível, de modo que as mudanças em um módulo tenham zero ou pouco impacto nos outros. Os módulos não devem ter conhecimento sobre o funcionamento interno uns dos outros.
- Coesão alta significa que os módulos têm que abranger um conjunto de códigos que atua como um sistema. Eles precisam ter responsabilidades definidas de forma clara e ficar dentro dos limites de um determinado conhecimento do domínio. Vamos usar um aplicativo de e-book como exemplo. Pode ser inadequado combinar códigos relacionados a livros e pagamentos no mesmo módulo desse app, porque esses são dois domínios funcionais diferentes.
Tipos de módulos
A maneira de organizar seus módulos depende principalmente da arquitetura do app. Veja abaixo alguns tipos comuns de módulos que você pode introduzir no seu app enquanto segue nossa arquitetura recomendada.
Módulos de dados
Um módulo de dados geralmente contém um repositório, fontes de dados e classes de modelo. As três principais responsabilidades de um módulo de dados são:
- Encapsular todos os dados e a lógica de negócios de um determinado domínio: cada módulo de dados precisa ser responsável por processar dados que representam um domínio específico. Vários tipos de dados podem ser processados, contanto que eles estejam relacionados.
- Expor o repositório como uma API externa: a API pública de um módulo de dados precisa ser um repositório, porque é responsável por expor os dados ao restante do app.
- Ocultar todos os detalhes de implementação e fontes de dados de fora:
as fontes de dados só podem ser acessadas por repositórios do mesmo módulo.
Elas continuam ocultas para o lado externo. Para aplicar isso, use a palavra-chave de visibilidade
private
ouinternal
do Kotlin.

Módulos de recursos
Um recurso é uma parte isolada da funcionalidade de um app que geralmente corresponde a uma tela ou várias intimamente relacionadas, como um fluxo de inscrição ou finalização de compra. Caso o app tenha uma navegação na barra da parte de baixo da tela, é provável que cada destino seja um recurso.

Os recursos estão associados a telas ou destinos no seu app. Portanto,
eles provavelmente têm uma IU associada e um ViewModel
para processar a lógica
e o estado. Um recurso não precisa estar limitado a uma única visualização ou
destino de navegação. Os módulos de recursos dependem dos módulos de dados.

Módulos do app
Os módulos do app são um ponto de entrada para ele. Eles dependem de módulos de recursos e geralmente fornecem navegação raiz. Um único módulo do app pode ser compilado para vários binários diferentes graças às variantes de build.

Caso seu app seja destinado a vários tipos de dispositivo, como automóveis, wearables ou TVs, é recomendável definir um módulo de app para cada um deles. Isso ajuda a separar as dependências específicas da plataforma.

Módulos comuns
Os módulos comuns, também conhecidos como módulos principais, contêm códigos que outros módulos usam com frequência. Eles diminuem a redundância e não representam nenhuma camada específica na arquitetura de um app. Estes são exemplos de módulos comuns:
- Módulo da IU: se você usa elementos de IU personalizados ou um branding elaborado no seu app, considere encapsular sua coleção de widgets em um módulo para que todos os recursos possam reutilizar. Isso pode ajudar a deixar a IU consistente em diferentes recursos. Por exemplo, no caso de um tema centralizado, você poderá evitar uma refatoração cansativa quando houver uma mudança de marca.
- Módulo de análise: o rastreamento geralmente é determinado por requisitos de negócios com pouca consideração à arquitetura do software. Os rastreadores de análise geralmente são usados em vários componentes não relacionados. Se esse for o caso, talvez seja uma boa ideia ter um módulo de análise dedicado.
- Módulo de rede: quando vários módulos exigirem uma conexão de rede, considere um módulo dedicado ao fornecimento de um cliente HTTP. Isso é especialmente útil quando o cliente exige uma configuração personalizada.
- Módulo de utilitário: os utilitários, também conhecidos como auxiliares, geralmente são pequenos códigos que são reutilizados no aplicativo. Exemplos de utilitários incluem auxiliares de teste, uma função de formatação de moeda, um validador de e-mail ou um operador personalizado.
Comunicação entre módulos
Os módulos raramente ficam totalmente separados. No geral, eles dependem de outros módulos e se comunicam com eles. É importante manter o acoplamento baixo mesmo quando os módulos trabalham juntos e trocam informações com frequência. Às vezes, a comunicação direta entre dois módulos não é recomendável, como no caso de restrições da arquitetura. Ela também pode ser impossível, como no caso de dependências cíclicas.

Para resolver esse problema, você pode ter um terceiro módulo mediando (link em inglês) dois outros módulos. O módulo mediador pode detectar e encaminhar mensagens dos dois módulos conforme necessário. Em nosso app de exemplo, a tela de finalização de compra precisa saber qual livro comprar mesmo que o evento seja originado em uma tela separada que faça parte de um recurso diferente. Nesse caso, o mediador é o módulo proprietário do gráfico de navegação, geralmente um módulo de app. No exemplo, usamos a navegação para transmitir os dados do recurso da página inicial para o de finalização da compra utilizando o componente de navegação.
navController.navigate("checkout/$bookId")
O destino de finalização de compra recebe um ID como um argumento usado para
buscar informações sobre o livro. Você pode usar o gerenciador de estado salvo para
recuperar argumentos de navegação dentro do ViewModel
de um recurso de destino.
class CheckoutViewModel(savedStateHandle: SavedStateHandle, …) : ViewModel() {
val uiState: StateFlow<CheckoutUiState> =
savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
// produce UI state calling bookRepository.getBook(bookId)
}
…
}
Não transmita objetos como argumentos de navegação. Em vez disso, use IDs simples com os quais os recursos possam acessar e carregar aquilo que você quer da camada de dados. Dessa forma, você mantém o acoplamento baixo e não viola o princípio de única fonte de informações.
No exemplo abaixo, os dois módulos de recursos dependem do mesmo módulo de dados. Isso permite minimizar a quantidade de dados que o módulo mediador precisa encaminhar e mantém o acoplamento baixo entre os módulos. Em vez de transmitir objetos, os módulos precisam trocar IDs primitivos e carregar os recursos de um módulo de dados compartilhado.

Práticas recomendadas gerais
Como mencionado no início, não há apenas uma maneira correta de desenvolver um app multimódulo. Assim como existem várias arquiteturas de software, há inúmeras formas de modularizar um app. No entanto, as recomendações gerais a seguir podem ajudar a deixar seu código mais legível, testável e de fácil manutenção.
Manter a consistência da configuração
Todo módulo introduz um overhead de configuração. Se o número de módulos atingir um determinado limite, o gerenciamento de configurações consistentes vai se tornar um desafio. Por exemplo, é importante que os módulos usem dependências da mesma versão. Se você precisa atualizar um grande número de módulos apenas para resolver a versão de uma dependência, isso não só exige um esforço considerável como também abre espaço para possíveis erros. Para resolver esse problema, use uma das ferramentas do Gradle e centralize sua configuração:
- Os catálogos de versões (link em inglês) são uma lista de tipos seguros de dependências geradas pelo Gradle durante a sincronização. Essa é uma plataforma central para declarar todas as dependências e está disponível para todos os módulos em um projeto.
- Use plug-ins de convenção (link em inglês) para compartilhar a lógica de build entre módulos.
Expor o mínimo possível
A interface pública de um módulo precisa ser mínima e expor apenas o
essencial. As informações de implementação não podem vazar. Defina
o escopo de tudo para a menor extensão possível. Use o escopo de visibilidade private
ou internal
do Kotlin para tornar o módulo de declarações particular. Ao declarar
dependências no módulo, prefira usar implementation
em vez de api
. Essa última
expõe dependências transitivas aos consumidores do módulo. O
uso da implementação pode melhorar o tempo de compilação porque reduz o número de módulos
que precisam ser recriados.
Preferir módulos Kotlin e Java
Há três tipos essenciais de módulos que podem ser usados no Android Studio:
- Os módulos do app são um ponto de entrada para o aplicativo. Eles podem conter o
código-fonte, recursos e um
AndroidManifest.xml
. A saída de um módulo do app é um Android App Bundle (AAB) ou um pacote de aplicativo Android (APK). - Os módulos de biblioteca têm o mesmo conteúdo dos módulos do app. Eles são usados por outros módulos do Android como uma dependência. A saída de um módulo de biblioteca é um ARchive do Android (AAR) estruturalmente idêntico aos módulos do app, mas compilado para um documento do AAR, que pode ser usado mais tarde por outros módulos como uma dependência. Um módulo de biblioteca permite encapsular e reutilizar a mesma lógica e recursos em vários módulos de apps.
- As bibliotecas Kotlin e Java não contêm recursos ou arquivos de manifesto do Android.
Como os módulos do Android vêm com overhead, recomendamos dar o máximo de preferência possível ao tipo Kotlin ou Java.