Modularização Android é uma daquelas decisões que parecem arquitetura pura, mas rapidamente viram assunto de produto. Um app Kotlin pequeno pode viver bem em um único módulo :app. Conforme o time cresce, as telas aumentam, o design system amadurece, os testes ficam mais caros e o CI passa a demorar demais, separar tudo em módulos Gradle começa a pagar a conta.

O problema é que modularizar cedo demais também cobra juros. Criar vinte módulos antes de entender as fronteiras reais do app aumenta boilerplate, dificulta navegação no código e cria dependências artificiais. Em 2026, a melhor abordagem para apps Android com Kotlin e Jetpack Compose é incremental: extrair módulos quando eles reduzem acoplamento, aceleram feedback ou deixam uma feature mais testável.

Este guia mostra um caminho prático para modularizar sem transformar o projeto em um labirinto. Se você ainda está montando a base, leia também Kotlin para Android, Clean Architecture em Kotlin e Gradle Build Cache em projetos Kotlin.

Quando modularizar um app Android

A pergunta boa não é “quantos módulos um app moderno precisa ter?”. A pergunta é: qual dor concreta o módulo resolve?

Modularização costuma fazer sentido quando:

  • o build do app completo está lento demais para feedback diário;
  • uma feature tem regras, telas e testes que mudam juntas;
  • o design system precisa ser reutilizado por várias áreas;
  • o time quer rodar testes de um domínio sem compilar o app inteiro;
  • dependências de infraestrutura estão vazando para camadas que deveriam ser simples;
  • múltiplos squads mexem no mesmo :app e geram conflitos frequentes;
  • o app precisa compartilhar lógica com KMP, Wear, TV ou backend auxiliar.

Se o único motivo é “arquitetura bonita”, espere. Um módulo cria uma API pública interna. APIs públicas exigem nomes melhores, documentação mínima e disciplina de dependências.

Um desenho inicial saudável

Um ponto de partida comum para Android Kotlin é separar o app em módulos de entrada, módulos compartilhados e módulos de feature:

:app
:core:designsystem
:core:model
:core:network
:core:database
:core:analytics
:feature:login
:feature:home
:feature:profile
:feature:checkout

O módulo :app deve ser fino. Ele inicializa aplicação, DI, tema global e navegação de alto nível. Ele não deveria concentrar regra de negócio, componentes visuais genéricos nem clients HTTP.

Módulos :core:* carregam capacidades compartilhadas. O :core:model pode conter modelos de domínio estáveis. O :core:designsystem reúne componentes Compose reutilizáveis, tokens, temas e previews. O :core:network sabe falar com APIs. O :core:database encapsula Room ou SQLDelight. O :core:analytics padroniza eventos.

Módulos :feature:* representam fluxos de produto. Uma feature pode depender de core, mas não deveria depender diretamente de outra feature sem um contrato claro.

Evite o grafo emaranhado

O maior risco da modularização é trocar um :app gigante por um grafo onde tudo depende de tudo. Para evitar isso, defina regras simples:

:app -> :feature:* -> :core:*
:feature:* -> :core:model
:feature:* -> :core:designsystem
:core:network -> :core:model
:core:database -> :core:model

Evite dependências laterais como :feature:checkout dependendo de :feature:profile. Se checkout precisa mostrar dados de perfil, crie um contrato em :core:model, :core:session ou um módulo de API menor. Dependência lateral parece prática no começo, mas vira ciclo conceitual rapidamente.

Uma regra útil é: feature depende de contrato, não de implementação de outra feature. Isso preserva autonomia e facilita testes.

Convention plugins para não repetir Gradle

Sem convention plugins, cada módulo novo vira um arquivo build.gradle.kts copiado e levemente diferente. Um módulo esquece jvmTarget, outro esquece Compose Compiler, outro usa dependências de teste desatualizadas. Isso derruba consistência e atrapalha cache.

Crie plugins internos no build-logic para padrões recorrentes:

// build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt
class AndroidFeatureConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) = with(target) {
        pluginManager.apply("com.android.library")
        pluginManager.apply("org.jetbrains.kotlin.android")
        pluginManager.apply("org.jetbrains.kotlin.plugin.compose")

        extensions.configure<LibraryExtension> {
            namespace = "br.com.exemplo.${project.name.replace('-', '.')}"
            compileSdk = 36

            defaultConfig {
                minSdk = 26
                testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
            }
        }
    }
}

Depois, nos módulos de feature:

plugins {
    id("exemplo.android.feature")
}

dependencies {
    implementation(projects.core.model)
    implementation(projects.core.designsystem)
}

Essa padronização combina com Kotlin com Gradle e Version Catalogs em Kotlin. O objetivo é que criar um módulo seja barato, mas consistente.

Compose ajuda, mas não elimina fronteiras

Jetpack Compose facilita dividir UI em componentes menores, mas componente não é a mesma coisa que módulo. Um botão, card ou skeleton loader pertence ao :core:designsystem. Uma tela de login pertence ao :feature:login. Um fluxo de onboarding talvez vire :feature:onboarding.

Use previews para manter o design system saudável:

@Preview(showBackground = true)
@Composable
private fun PrimaryButtonPreview() {
    AppTheme {
        PrimaryButton(
            text = "Continuar",
            onClick = {}
        )
    }
}

Quando o design system fica em módulo próprio, previews, testes de screenshot e documentação visual podem evoluir sem recompilar todas as features. Se você já está nessa fase, o artigo de testes de screenshot no Compose é o próximo passo natural.

Injeção de dependência entre módulos

Hilt, Koin ou DI manual funcionam em projetos modularizados, mas a regra é a mesma: módulos de feature não deveriam conhecer detalhes desnecessários.

Com Hilt, mantenha bindings próximos da implementação e exponha contratos estáveis:

interface ProfileRepository {
    suspend fun currentProfile(): Profile
}

@Module
@InstallIn(SingletonComponent::class)
abstract class ProfileDataModule {
    @Binds
    abstract fun bindProfileRepository(
        impl: NetworkProfileRepository
    ): ProfileRepository
}

Se uma feature só precisa de ProfileRepository, ela não precisa saber se os dados vêm de Retrofit, Ktor Client, Room ou DataStore. Para aprofundar a escolha entre ferramentas, veja Hilt no Android com Kotlin e Koin ou Hilt em Kotlin.

Testes ficam mais baratos quando a fronteira é boa

Um benefício real da modularização é rodar testes menores. Se :feature:checkout concentra UI, view models e casos de uso daquele fluxo, o CI pode executar:

./gradlew :feature:checkout:testDebugUnitTest
./gradlew :feature:checkout:lintDebug

Isso reduz feedback em pull requests. Mas só funciona se as dependências forem limpas. Se o módulo de checkout depende de metade do app, o teste continua pesado.

Para módulos de domínio, prefira testes JVM simples com JUnit, MockK e coroutines test. Para módulos de UI, combine testes de view model, previews revisáveis e screenshot tests quando o custo se justificar. O guia de testes Android com Compose e Maestro ajuda a decidir onde entra cada camada.

CI/CD por módulos afetados

Depois que o grafo está estável, vale evoluir o pipeline. O primeiro passo é continuar rodando ./gradlew check em main. Em pull requests, você pode detectar arquivos alterados e priorizar módulos impactados.

Exemplo simplificado:

./gradlew :feature:home:check :core:designsystem:check

Não pule validação global cedo demais. Em times pequenos, um pipeline completo pode ser mais simples e seguro. Em monorepos grandes, a seleção por módulos afetados economiza muito tempo, especialmente quando combinada com Gradle build cache e configuration cache. O guia de CI/CD para Kotlin cobre essa base.

Sinais de overengineering

Modularização deve deixar o projeto mais fácil de mudar. Se ela está piorando tudo, os sinais aparecem:

  • criar uma tela simples exige mexer em cinco módulos;
  • nomes de módulos refletem camadas abstratas demais, não produto real;
  • quase todo módulo depende de :core:common com centenas de utilitários;
  • há ciclos ou dependências indiretas difíceis de explicar;
  • o tempo de configuração do Gradle aumentou mais do que o tempo economizado;
  • a navegação entre arquivos ficou mais lenta para o time;
  • testes continuam lentos porque os módulos não têm fronteiras reais.

Nesses casos, simplifique. Junte módulos pequenos demais, divida common por responsabilidade e documente as regras de dependência no README técnico.

Checklist prático

Antes de extrair um módulo, responda:

  1. Qual dor concreta esse módulo resolve?
  2. Quem pode depender dele?
  3. Ele expõe contrato ou implementação?
  4. Quais dependências ficam proibidas?
  5. Quais testes devem rodar isoladamente?
  6. O módulo melhora ou piora o tempo de build?
  7. O nome descreve produto/capacidade real?

Se as respostas forem vagas, talvez ainda seja cedo.

Perguntas frequentes

Todo app Android Kotlin precisa ser modularizado?

Não. Apps pequenos podem viver bem em um único módulo por bastante tempo. Modularize quando houver dor real de build, testes, organização, reuso ou colaboração.

Devo separar por camada ou por feature?

Para apps de produto, módulos por feature costumam ser mais úteis. Camadas compartilhadas entram em core ou módulos de domínio. Separar apenas por data, domain e presentation no app inteiro pode criar acoplamento transversal e dificultar ownership.

Modularização melhora performance do app?

Indiretamente. Ela melhora build, testes e organização. Performance de runtime depende de código, inicialização, tamanho de APK/AAB, baseline profiles e uso correto de APIs Android. Modularização ajuda a controlar isso, mas não substitui medição.

Dynamic Feature Modules ainda valem a pena?

Valem quando você realmente precisa entregar partes do app sob demanda para reduzir download inicial ou separar fluxos raros. Não use dynamic feature apenas para organizar código; módulos Gradle normais já resolvem organização sem complexidade extra de entrega.

Quantos módulos são demais?

Não existe número universal. O limite aparece quando o overhead de navegação, configuração e dependências supera o ganho de isolamento. Comece com poucos módulos de alto valor e extraia novos conforme o app pedir.

Conclusão

Modularização Android com Kotlin é uma ferramenta de escala, não um troféu arquitetural. Comece pelo problema: build lento, feature grande demais, design system compartilhado, testes caros ou fronteiras confusas. Extraia módulos pequenos, com contratos claros, Gradle padronizado e dependências previsíveis.

O melhor projeto modularizado é aquele em que uma pessoa consegue mudar uma feature, rodar testes relevantes, entender impactos e abrir pull request sem carregar o app inteiro nas costas. Em 2026, Kotlin, Compose e Gradle já dão a base técnica. O diferencial é usar essa base com pragmatismo.