Resposta rápida: use o Play Feature Delivery quando uma funcionalidade grande do app Android não precisa estar no primeiro download. Você move essa funcionalidade para um módulo com.android.dynamic-feature, define a entrega como install-time, fast-follow ou on-demand, gera um Android App Bundle e deixa o Google Play produzir APKs otimizados para cada aparelho. Em Kotlin, recursos on-demand são solicitados com a Play Core API, normalmente por meio de um SplitInstallManager, enquanto a interface acompanha download, confirmação, instalação e erro. O ganho pode ser relevante em jogos, editores, mapas, conteúdo regional e fluxos raramente usados, mas módulos dinâmicos adicionam estados de rede, testes e complexidade operacional. Não modularize apenas para “organizar pastas”: primeiro meça o tamanho e escolha uma fronteira de produto realmente independente.
O tamanho do aplicativo influencia aquisição e atualização. Um app que inclui modelos de machine learning, editores completos, mapas, recursos para vários países e fluxos administrativos pode obrigar todo usuário a baixar código e recursos que talvez nunca use. Ao mesmo tempo, separar tudo em módulos baixados pela internet pode produzir telas quebradas, navegação imprevisível e suporte difícil.
O Play Feature Delivery resolve uma parte desse problema ao combinar Android App Bundle, módulos dynamic feature e entrega administrada pelo Google Play. O módulo base continua instalável; funcionalidades adicionais podem chegar junto com o app, logo depois da instalação ou somente quando a pessoa solicitar. Este guia mostra como tomar a decisão, criar um módulo, pedir instalação em Kotlin, integrar com Jetpack Compose e testar o fluxo sem confundir modularização arquitetural com entrega dinâmica.
O que é Play Feature Delivery?
Play Feature Delivery é o mecanismo do Google Play para entregar módulos funcionais de um Android App Bundle em momentos diferentes. Em vez de publicar um APK universal com tudo, o projeto gera um .aab. O Google Play usa esse bundle para montar e servir artefatos adequados à configuração do dispositivo e às regras de entrega declaradas pelo app.
Um dynamic feature module pode conter:
- código Kotlin e Java;
- telas com Views ou Jetpack Compose;
- recursos como strings, drawables e layouts;
- dependências específicas da funcionalidade;
- manifesto próprio combinado com o módulo base;
- componentes Android necessários ao fluxo.
Ele não é um aplicativo independente. O módulo depende do módulo base e passa a fazer parte do app instalado depois que sua entrega termina.
A tecnologia é diferente de Play Asset Delivery, voltada a pacotes grandes de assets, especialmente em jogos. Se a necessidade é entregar texturas, áudio ou outros arquivos sem código executável, avalie asset packs. Se a unidade contém código e recursos de uma funcionalidade Android, dynamic feature é a opção relevante.
Os três modos de entrega
A primeira decisão é quando o módulo deve chegar ao aparelho.
| Modo | Quando é entregue | Caso típico |
|---|---|---|
install-time | durante a instalação inicial | funcionalidade obrigatória segmentada por condição |
fast-follow | automaticamente após a instalação do app | conteúdo importante, mas dispensável para a primeira abertura |
on-demand | quando o app solicita | editor avançado, mapa de uma região ou fluxo raramente usado |
Install-time
O módulo acompanha a instalação inicial para os dispositivos elegíveis. Ele ainda pode ser útil para entrega condicional por país, capacidade do aparelho ou versão do Android, mas não reduz o tempo inicial para quem recebe o módulo.
Fast-follow
O download começa depois que a instalação principal termina, sem exigir que o usuário abra a funcionalidade. É útil quando a primeira experiência funciona sem o módulo, mas a chance de uso em seguida é alta.
On-demand
O app solicita o módulo em runtime. Esse modo oferece o maior potencial de evitar downloads desnecessários, porém cria mais estados de produto: sem rede, aguardando Wi-Fi, confirmação necessária, download em progresso, instalação concluída, cancelamento e falha.
Não escolha on-demand só porque parece tecnicamente sofisticado. Se quase todos usam a funcionalidade na primeira sessão, adiar o módulo pode apenas trocar alguns megabytes por frustração.
Quando módulos dinâmicos valem a pena?
Bons candidatos costumam reunir três características:
- têm peso mensurável no bundle;
- são usados por uma parte menor da base ou somente mais tarde;
- possuem uma fronteira funcional compreensível.
Exemplos possíveis:
- scanner ou editor com bibliotecas nativas grandes;
- recursos de realidade aumentada;
- modelos locais de inteligência artificial;
- mapas e fluxos específicos por região;
- módulo administrativo usado por poucos perfis;
- central de criação de vídeo ou áudio;
- experiência avançada ativada após assinatura;
- conteúdo de treinamento baixado por demanda.
Evite começar por uma feature central, como login, home ou checkout. Também não transforme cada tela em um módulo dinâmico. Muitos módulos pequenos aumentam configuração do Gradle, contratos entre módulos, matrizes de teste e pontos de falha.
Antes de decidir, analise o tamanho com ferramentas do Android Studio e revise dependências desnecessárias. Às vezes, R8 e resource shrinking resolvem o problema com muito menos complexidade. Baseline Profiles também têm outro objetivo: melhorar execução e startup, não remover bytes do primeiro download. Veja o guia de Baseline Profiles e Macrobenchmark.
Módulo de biblioteca não é dynamic feature
Uma confusão comum é tratar qualquer modularização como entrega sob demanda.
Um módulo com.android.library ajuda a separar responsabilidades, acelerar builds em determinadas condições e controlar dependências. Porém, seu código normalmente é empacotado com o aplicativo que o consome.
Um módulo com.android.dynamic-feature também cria uma fronteira, mas participa do App Bundle com regras de entrega próprias. Essa diferença afeta navegação, acesso a recursos, injeção de dependência, publicação e testes.
Uma estratégia segura é amadurecer primeiro a modularização Android com Kotlin e Compose. Quando a fronteira estiver estável e o ganho de download estiver comprovado, transforme um candidato em dynamic feature. Fazer arquitetura e delivery mudarem ao mesmo tempo dificulta descobrir a origem de problemas.
Criando um dynamic feature module
Considere um projeto com módulo base :app e uma funcionalidade chamada :feature-editor.
No módulo da feature, use o plugin apropriado:
plugins {
id("com.android.dynamic-feature")
kotlin("android")
}
android {
namespace = "br.dev.kotlin.editor"
}
dependencies {
implementation(project(":app"))
}
O módulo dynamic feature depende do base. O caminho contrário não pode ser uma dependência direta comum, porque o código da feature talvez não esteja instalado. O módulo base precisa conhecer apenas um contrato, uma rota ou um mecanismo de entrada que continue válido quando a implementação ainda não existe no dispositivo.
No módulo base, declare a feature dinâmica na configuração Android conforme a versão atual do Android Gradle Plugin:
android {
dynamicFeatures += setOf(":feature-editor")
}
Use versões compatíveis do Android Gradle Plugin, Gradle e bibliotecas Play. Consulte a documentação oficial de Play Feature Delivery antes de fixar números, pois APIs e requisitos de publicação evoluem.
Configurando a entrega no manifesto
O manifesto do dynamic feature informa ao bundle como a funcionalidade será entregue. Para uma feature on-demand, o conceito é:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution">
<dist:module
dist:instant="false"
dist:title="@string/title_editor">
<dist:delivery>
<dist:on-demand />
</dist:delivery>
<dist:fusing dist:include="true" />
</dist:module>
<application />
</manifest>
O valor usado para solicitar a instalação é o nome do módulo, como feature_editor, e não necessariamente o package ou a rota de navegação. Confirme o nome final gerado pelo projeto e centralize-o em uma constante para evitar divergência.
Para entrega install-time, o bloco muda para a modalidade correspondente. Regras condicionais precisam ser cuidadosamente validadas, porque determinam quais dispositivos recebem a feature.
Solicitando instalação em Kotlin
A API de instalação expõe estados assíncronos. Encapsule-a em uma classe própria em vez de chamar Play Core diretamente de cada Activity ou Composable.
private const val EDITOR_MODULE = "feature_editor"
class DynamicFeatureInstaller(
context: Context,
) {
private val manager = SplitInstallManagerFactory.create(context)
fun isEditorInstalled(): Boolean =
EDITOR_MODULE in manager.installedModules
fun installEditor(
onState: (SplitInstallSessionState) -> Unit,
onRequestFailed: (Exception) -> Unit,
) {
if (isEditorInstalled()) return
val request = SplitInstallRequest.newBuilder()
.addModule(EDITOR_MODULE)
.build()
manager.startInstall(request)
.addOnSuccessListener { sessionId ->
// O listener de estados acompanha a sessão pelo ID.
}
.addOnFailureListener(onRequestFailed)
}
fun registerListener(
listener: SplitInstallStateUpdatedListener,
) = manager.registerListener(listener)
fun unregisterListener(
listener: SplitInstallStateUpdatedListener,
) = manager.unregisterListener(listener)
}
As classes e artefatos exatos dependem da geração atual das bibliotecas Play. Trate o exemplo como estrutura e valide os imports na documentação oficial.
O retorno de startInstall significa que uma solicitação foi criada, não que o módulo está pronto. O app só deve navegar para o código dinâmico depois do estado de instalação concluída.
Modelando estados sem expor detalhes à UI
A tela precisa de um estado de domínio menor e previsível:
sealed interface FeatureInstallState {
data object NotInstalled : FeatureInstallState
data object Requesting : FeatureInstallState
data class Downloading(val progress: Int) : FeatureInstallState
data object Installing : FeatureInstallState
data object Installed : FeatureInstallState
data class Failed(val canRetry: Boolean) : FeatureInstallState
}
Converta bytes baixados em percentual com proteção contra divisão por zero:
fun progressOf(state: SplitInstallSessionState): Int {
val total = state.totalBytesToDownload()
if (total <= 0L) return 0
return ((state.bytesDownloaded() * 100L) / total)
.coerceIn(0L, 100L)
.toInt()
}
Mapeie os estados da biblioteca para a linguagem do produto:
- solicitação pendente;
- aguardando confirmação;
- baixando;
- instalando;
- instalado;
- cancelado;
- falha recuperável;
- falha que exige atualização ou suporte.
Não mostre códigos internos ao usuário. Registre-os em observabilidade sem dados pessoais e apresente uma ação útil, como “Tentar novamente” ou “Conectar ao Wi-Fi”.
Confirmação do usuário e downloads maiores
Em algumas condições, o Google Play pode exigir confirmação antes de continuar o download. O app precisa reconhecer esse estado e abrir o fluxo de confirmação indicado pela API, usando uma Activity válida.
Isso afeta arquitetura: um repository com applicationContext pode acompanhar instalação, mas uma confirmação visual precisa da camada de UI. Modele um efeito separado em vez de guardar Activity dentro de singleton.
Também não prometa que o módulo estará disponível imediatamente. Rede lenta, falta de armazenamento, restrições do sistema ou indisponibilidade do Google Play podem interromper o fluxo. A feature precisa ter fallback ou explicar claramente por que não pode abrir.
Integração com Jetpack Compose
Compose deve renderizar estado; não deve iniciar uma instalação no corpo de uma função composable.
@Composable
fun EditorEntryRoute(
state: FeatureInstallState,
onInstall: () -> Unit,
onOpenEditor: () -> Unit,
onRetry: () -> Unit,
) {
when (state) {
FeatureInstallState.NotInstalled -> Button(onClick = onInstall) {
Text("Baixar editor")
}
FeatureInstallState.Requesting,
FeatureInstallState.Installing -> CircularProgressIndicator()
is FeatureInstallState.Downloading -> Column {
Text("Baixando editor: ${state.progress}%")
LinearProgressIndicator(
progress = { state.progress / 100f },
)
}
FeatureInstallState.Installed -> Button(onClick = onOpenEditor) {
Text("Abrir editor")
}
is FeatureInstallState.Failed -> ErrorCard(
message = "Não foi possível instalar o editor agora.",
onRetry = onRetry,
)
}
}
O ViewModel ou uma classe de coordenação registra o listener, atualiza um StateFlow e remove o listener quando não for mais necessário. Se a instalação continuar durante recriação da tela, reconecte ao estado da sessão em vez de iniciar outro download automaticamente.
Como navegar para uma feature que pode não existir
A navegação precisa respeitar duas fases:
- garantir que o módulo está instalado;
- abrir o ponto de entrada da funcionalidade.
Uma opção é expor uma Activity no manifesto da feature e iniciar uma Intent somente após a instalação. Outra é integrar rotas Compose por meio de contratos e registro de destinos. A escolha depende da arquitetura e da versão das bibliotecas de navegação.
Evite o módulo base importar diretamente classes da feature. Um contrato simples pode viver no base ou em um módulo de API instalado desde o início:
interface EditorLauncher {
fun open(context: Context)
}
A implementação concreta só pode ser carregada quando a feature estiver disponível. Reflection e service locators podem funcionar, mas precisam de tratamento explícito para classe ausente e regras de R8. Em projetos grandes, um ponto de entrada Android declarado costuma ser mais simples de operar.
Se o app usa Compose, leia também o guia de Navigation 3 no Android e mantenha a regra “download antes da rota” em um coordenador, não espalhada por várias telas.
Injeção de dependência entre base e feature
Dynamic features exigem direção de dependência clara. O módulo pode consumir objetos fornecidos pelo base, mas o base não deve depender da implementação dinâmica.
Com Hilt ou Dagger, isso pode exigir componentes, entry points ou contratos definidos em um módulo comum. Com Koin, módulos podem ser carregados depois da instalação, mas você ainda precisa impedir que uma tela tente resolver uma dependência antes de registrar o módulo.
Independentemente do framework:
- interfaces compartilhadas ficam em uma camada instalada;
- implementações pesadas ficam na feature;
- o ciclo de vida do container deve ser conhecido;
- uma instalação falha não pode derrubar a home;
- testes devem cobrir feature ausente e presente.
O guia de Hilt com módulos, scopes e testes ajuda a revisar as fronteiras antes de adicionar entrega dinâmica.
Recursos, idiomas e acesso ao módulo
Recursos do base podem ser usados pela feature, mas o contrário exige que a feature esteja instalada. Não coloque no módulo on-demand uma string ou drawable necessário à tela que convida a pessoa a baixá-lo.
Mantenha no base:
- título e descrição exibidos antes do download;
- ícone e estado de erro;
- textos de acessibilidade;
- fallback de navegação;
- analytics do pedido de instalação.
A feature pode conter recursos usados somente depois da instalação. Ao trabalhar com idiomas e configurações, teste a troca de locale e o comportamento de recursos já baixados. Não presuma que o cenário do emulador representa todas as combinações servidas pelo Google Play.
Como testar dynamic features
Executar apenas app-debug.apk pelo Android Studio não reproduz necessariamente o comportamento real de um App Bundle publicado. O processo de validação deve ter camadas.
Teste a lógica com fake
Crie uma abstração para instalação:
interface FeatureInstaller {
fun observe(): Flow<FeatureInstallState>
suspend fun request(moduleName: String)
suspend fun cancel()
fun isInstalled(moduleName: String): Boolean
}
Nos testes de ViewModel, use um fake que emite Downloading, Installed e Failed. Isso permite validar UI, retry e navegação sem depender do Google Play.
Teste o bundle localmente
Gere um Android App Bundle e use as ferramentas oficiais de bundle para criar e instalar conjuntos de APKs em um dispositivo ou emulador. Esse caminho ajuda a validar splits e configuração do manifesto.
Use compartilhamento interno ou faixa de teste
Para reproduzir a entrega administrada pelo Google Play, publique em um fluxo de teste apropriado. Valide com contas e dispositivos controlados antes de promover para produção.
Matriz mínima
Teste pelo menos:
- módulo ausente e rede disponível;
- download lento;
- falta de espaço;
- cancelamento;
- rotação e recriação da Activity;
- processo encerrado durante instalação;
- confirmação exigida;
- módulo já instalado;
- atualização do app com módulo antigo presente;
- navegação por deep link para feature ausente;
- aparelho sem ambiente Google Play compatível, se distribuído fora da loja.
Para automação de interface, combine testes instrumentados com Espresso no Android e Compose, mas mantenha a maior parte da máquina de estados testável sem infraestrutura externa.
Distribuição fora do Google Play
Play Feature Delivery depende da infraestrutura do Google Play. Se o mesmo app é distribuído por MDM, loja de fabricante, download direto ou outro canal, confirme como aquele canal trata App Bundles e módulos dinâmicos.
Uma empresa pode precisar de variantes diferentes:
- distribuição Play com feature on-demand;
- distribuição corporativa com a funcionalidade incluída install-time;
- aplicativo separado para um fluxo muito específico.
Não descubra essa limitação depois de estruturar o produto inteiro em torno de downloads Play. A estratégia de distribuição faz parte da decisão arquitetural.
Observabilidade e métricas de produto
Meça o funil da feature sem registrar dados pessoais:
- convite ao download exibido;
- pessoa solicitou instalação;
- confirmação foi necessária;
- download começou;
- instalação terminou;
- funcionalidade abriu;
- erro ou cancelamento ocorreu.
A taxa “instalou e abriu” é mais útil que contar apenas solicitações. Compare também o tamanho economizado no download inicial e o percentual de usuários que acabam instalando o módulo. Se 95% baixa a feature imediatamente, o custo operacional talvez não compense.
Monitore códigos de erro agregados por versão do app e condição de rede, com cardinalidade controlada. O guia de Firebase Performance Monitoring mostra como instrumentar jornadas sem colocar identificadores em atributos.
Erros comuns
Criar dynamic feature sem medir o bundle
A solução pode adicionar meses de manutenção para economizar pouco. Meça tamanho por módulo e dependência antes.
Colocar fluxo essencial on-demand
Login, home e checkout não devem depender de download inesperado sem uma justificativa muito forte.
Navegar antes do estado instalado
A solicitação aceita não significa que classes e recursos estão disponíveis.
Iniciar instalação em recomposição
Em Compose, use evento do usuário e estado no ViewModel. Recomposição não é comando de download.
Acoplar o base à implementação dinâmica
O base sempre está presente; a feature não. A direção de dependência deve refletir isso.
Ignorar distribuição alternativa
Um app corporativo fora do Google Play pode não ter o mesmo mecanismo de entrega.
Não testar atualização e processo morto
A instalação atravessa eventos de ciclo de vida. A UI precisa se reconectar ao estado existente.
Usar módulos dinâmicos como controle de acesso
Não confie na ausência do módulo como autorização. Permissões e regras de negócio continuam no backend e nas camadas de segurança do app.
Checklist para produção
- o tamanho do bundle foi medido antes da decisão;
- a feature tem fronteira funcional independente;
- o modo install-time, fast-follow ou on-demand foi escolhido por comportamento real;
- o módulo base funciona sem a feature;
- contratos não criam dependência do base para a implementação dinâmica;
- UI trata download, confirmação, instalação, cancelamento e erro;
- navegação só acontece após instalação concluída;
- listeners são registrados e removidos com ciclo de vida correto;
- processo morto e recriação de tela foram testados;
- deep links para feature ausente têm fallback;
- R8 e recursos foram validados no bundle de release;
- distribuição fora do Google Play foi considerada;
- métricas medem solicitação, sucesso, abertura e falha;
- a economia no download inicial compensa a complexidade;
- o rollout começa em uma faixa de teste controlada.
Perguntas frequentes
Dynamic feature reduz automaticamente o tamanho do app?
Ela pode reduzir o download inicial quando código e recursos saem do módulo base e são entregues depois. O ganho real depende do peso da feature, de dependências compartilhadas e das regras do bundle. Meça o artefato de release.
Preciso usar Jetpack Compose?
Não. Play Feature Delivery funciona com Views, Activities, Fragments ou Compose. O cuidado comum é não abrir componentes antes de o módulo estar instalado.
Posso baixar uma feature sem Google Play?
O mecanismo Play Feature Delivery é ligado ao Google Play. Outros canais podem exigir entrega install-time, outro sistema de distribuição ou uma arquitetura diferente.
O módulo on-demand pode ser removido depois?
A plataforma possui mecanismos relacionados à desinstalação adiada de módulos instalados sob demanda. Confirme o comportamento e a API atuais antes de oferecer isso como ação ao usuário, pois remoção não deve ocorrer enquanto a feature está em uso.
Dynamic feature substitui modularização por bibliotecas?
Não. Módulos de biblioteca resolvem organização e dependências no build. Dynamic features acrescentam entrega separada. Um projeto pode usar os dois modelos.
Posso proteger uma funcionalidade paga apenas deixando o módulo fora do aparelho?
Não. A autorização deve ser validada pelo produto e, quando aplicável, pelo backend. O módulo instalado não pode ser tratado como prova definitiva de assinatura ativa.
Como escolher entre fast-follow e on-demand?
Use fast-follow quando a funcionalidade é provavelmente necessária logo depois da instalação, mas não para a primeira abertura. Use on-demand quando o uso é opcional, tardio ou restrito a poucos usuários e existe uma UX aceitável para download.
Conclusão e próximos passos
Play Feature Delivery é uma ferramenta de distribuição, não uma meta arquitetural. Ela funciona melhor quando o time já conhece o peso do aplicativo, possui uma fronteira modular estável e consegue explicar por que aquela funcionalidade não deve acompanhar o primeiro download.
Comece com um candidato mensurável. Extraia a feature para uma fronteira clara, compare o App Bundle antes e depois e implemente uma máquina de estados pequena. Mantenha convite, progresso e fallback no módulo base; só navegue após instalação concluída; teste processo morto, atualização e distribuição real pela Play Store.
Se a economia não compensar, volte atrás: reduzir dependências, ativar shrinking ou reorganizar recursos pode ser a solução mais simples. Se compensar, avance com rollout controlado e métricas do funil completo.
Para continuar a trilha, leia modularização Android com Kotlin e Compose, R8 e ProGuard, Baseline Profiles e Macrobenchmark e Android Studio Profiler. Em um projeto de portfólio, documente o tamanho inicial, a feature extraída, o funil de instalação e a economia medida: isso demonstra decisão de engenharia, não apenas conhecimento de API.