---
title: "Google Play Billing com Kotlin e Compose: compras e assinaturas em 2026"
url: "https://kotlin.dev.br/blog/play-billing-kotlin-compose-assinaturas-2026/"
markdown_url: "https://kotlin.dev.br/blog/play-billing-kotlin-compose-assinaturas-2026.MD"
description: "Implemente Google Play Billing com Kotlin e Jetpack Compose: produtos, assinaturas, ProductDetails, confirmação, restauração e validação segura no backend."
date: "2026-08-12"
author: "Karina Melo"
---

# Google Play Billing com Kotlin e Compose: compras e assinaturas em 2026

Implemente Google Play Billing com Kotlin e Jetpack Compose: produtos, assinaturas, ProductDetails, confirmação, restauração e validação segura no backend.


**Resposta rápida:** para implementar **Google Play Billing com Kotlin e Jetpack Compose**, trate a cobrança como um fluxo distribuído, não como um botão que “desbloqueia premium”. O app conecta ao `BillingClient`, consulta `ProductDetails`, abre o checkout com `launchBillingFlow`, recebe a `Purchase`, envia o token ao backend para validação, entrega o benefício e confirma a compra com `acknowledgePurchase` quando necessário. Na inicialização e no retorno ao app, consulte compras existentes para restaurar acesso. Para assinaturas, o servidor precisa acompanhar renovação, cancelamento, período de carência e expiração; confiar apenas no estado salvo no celular permite fraude e perde mudanças ocorridas fora do app.

A Google Play Billing Library é o caminho oficial para vender **produtos digitais** em apps distribuídos pela Google Play: versão premium, moedas virtuais, recursos extras e assinaturas. Ela não substitui um gateway comum para produtos físicos, delivery ou serviços consumidos fora do app; as políticas da loja e a categoria do produto determinam qual fluxo usar.

Este guia monta uma base moderna em Kotlin: arquitetura, catálogo, conexão, `ProductDetails`, compra com Compose, restauração, confirmação, backend e testes. Ele combina bem com [MVVM no Android](/guias/guia-arquitetura-mvvm-kotlin/), [segurança de dados locais](/blog/seguranca-dados-locais-android-kotlin-2026/) e [autenticação biométrica](/blog/biometricprompt-android-kotlin-biometria-2026/) para proteger ações sensíveis sem confundir biometria com validação de pagamento.

## O que você pode vender pela Google Play

Antes do código, modele o produto corretamente no Play Console. Os dois grupos principais são:

| Tipo | Exemplo | Estado esperado |
|---|---|---|
| Produto de compra única | remover anúncios, pacote de temas, créditos | permanente ou consumível |
| Assinatura | plano mensal de curso, backup premium, recursos Pro | renovável, com planos e ofertas |

Um produto de compra única pode ser **não consumível**, como “versão premium para sempre”, ou **consumível**, como um pacote de moedas. O item consumível precisa ser consumido depois da concessão para poder ser comprado novamente. Uma assinatura exige tratamento de ciclo de vida: renovação, cancelamento, pausa, troca de plano, carência e expiração.

Não codifique preço, moeda ou texto promocional no APK. Consulte `ProductDetails` e mostre os dados devolvidos pela Play. Assim o usuário vê o valor localizado para o país e a oferta elegível para a conta.

## Arquitetura recomendada

Uma implementação sustentável separa responsabilidades:

1. **Play Console:** define IDs, planos-base e ofertas.
2. **BillingClient:** conversa com a Play Store no aparelho.
3. **BillingRepository:** transforma callbacks em estado Kotlin e eventos de domínio.
4. **ViewModel:** expõe `StateFlow` para a tela Compose.
5. **Backend:** valida token, registra direito de acesso e processa mudanças posteriores.
6. **UI:** apresenta preço, inicia checkout e mostra estados; não decide sozinha se o usuário é premium.

O fluxo seguro fica assim:

```text
Compose -> ViewModel -> BillingRepository -> Google Play
                                  |
                                  v
                             token da compra
                                  |
                                  v
                         Backend valida e concede
                                  |
                                  v
                     app recebe entitlement atual
```

**Entitlement** é o direito efetivo do usuário: “premium ativo”, “100 créditos disponíveis” ou “assinatura válida até determinada condição”. Compra e entitlement não são sinônimos. Uma compra pode estar pendente, cancelada, reembolsada ou ainda não validada.

## Dependência e configuração inicial

Use a versão estável da Billing Library exigida atualmente pela Google Play. Como a política de versões muda ao longo do tempo, centralize o número no Version Catalog e confira as notas oficiais antes de publicar:

```toml
# gradle/libs.versions.toml
[versions]
playBilling = "<versao-estavel-atual>"

[libraries]
play-billing-ktx = {
  module = "com.android.billingclient:billing-ktx",
  version.ref = "playBilling"
}
```

```kotlin
// build.gradle.kts do módulo app
dependencies {
    implementation(libs.play.billing.ktx)
}
```

O Billing não faz parte do [Compose BOM](/blog/compose-bom-android-kotlin-guia-2026/). O BOM alinha bibliotecas Compose; a Billing Library mantém seu próprio ciclo de versão.

Defina IDs estáveis em um único lugar. Eles devem coincidir exatamente com o Play Console:

```kotlin
object BillingProducts {
    const val PREMIUM_LIFETIME = "premium_lifetime"
    const val PRO_SUBSCRIPTION = "pro_subscription"
}
```

Evite espalhar strings por composables, fragments e workers. Trocar um ID publicado é uma migração de produto, não uma refatoração comum.

## Criando e conectando o BillingClient

O repositório implementa `PurchasesUpdatedListener`, porque o resultado da compra pode chegar depois que o checkout foi aberto:

```kotlin
class PlayBillingRepository(
    private val context: Context,
    private val backend: BillingBackend,
    private val appScope: CoroutineScope,
) : PurchasesUpdatedListener {

    private val _state = MutableStateFlow(BillingUiState())
    val state: StateFlow<BillingUiState> = _state.asStateFlow()

    private val billingClient = BillingClient.newBuilder(context)
        .setListener(this)
        .enablePendingPurchases()
        .build()

    fun connect() {
        if (billingClient.isReady) return

        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(result: BillingResult) {
                if (result.responseCode == BillingClient.BillingResponseCode.OK) {
                    appScope.launch {
                        loadProducts()
                        restorePurchases()
                    }
                } else {
                    _state.update {
                        it.copy(error = "Não foi possível conectar à Google Play.")
                    }
                }
            }

            override fun onBillingServiceDisconnected() {
                _state.update { it.copy(connected = false) }
                // Agende retry com backoff enquanto a tela/feature estiver ativa.
            }
        })
    }
}
```

Algumas regras importantes:

- conexão pode cair; implemente retry controlado, sem loop agressivo;
- não crie um `BillingClient` a cada recomposição;
- mantenha o client no escopo da aplicação ou de um repository de longa vida;
- encerre a conexão quando o dono real do objeto for destruído;
- não presuma que Play Store e conta Google estão disponíveis em todo aparelho.

Um estado simples para a UI:

```kotlin
data class BillingUiState(
    val connected: Boolean = false,
    val loading: Boolean = true,
    val products: List<ProductDetails> = emptyList(),
    val premiumActive: Boolean = false,
    val purchaseInProgress: Boolean = false,
    val message: String? = null,
    val error: String? = null,
)
```

## Consultando ProductDetails

Consulte separadamente produtos de compra única (`INAPP`) e assinaturas (`SUBS`):

```kotlin
private suspend fun queryProductDetails(
    productIds: List<String>,
    productType: String,
): List<ProductDetails> {
    val products = productIds.map { id ->
        QueryProductDetailsParams.Product.newBuilder()
            .setProductId(id)
            .setProductType(productType)
            .build()
    }

    val params = QueryProductDetailsParams.newBuilder()
        .setProductList(products)
        .build()

    val result = billingClient.queryProductDetails(params)
    if (result.billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
        throw IllegalStateException(result.billingResult.debugMessage)
    }

    return result.productDetailsList.orEmpty()
}

private suspend fun loadProducts() {
    _state.update { it.copy(loading = true, error = null) }

    runCatching {
        val oneTime = queryProductDetails(
            productIds = listOf(BillingProducts.PREMIUM_LIFETIME),
            productType = BillingClient.ProductType.INAPP,
        )
        val subscriptions = queryProductDetails(
            productIds = listOf(BillingProducts.PRO_SUBSCRIPTION),
            productType = BillingClient.ProductType.SUBS,
        )
        oneTime + subscriptions
    }.onSuccess { products ->
        _state.update {
            it.copy(connected = true, loading = false, products = products)
        }
    }.onFailure { cause ->
        _state.update {
            it.copy(loading = false, error = cause.message ?: "Catálogo indisponível")
        }
    }
}
```

Se um produto não aparece, confira antes de culpar o Kotlin:

1. ID e tipo estão corretos?
2. Produto/plano está ativo no Play Console?
3. A build foi instalada por uma faixa de teste da Play?
4. A conta do aparelho é testadora e elegível?
5. O app usa o mesmo `applicationId` e assinatura esperados?
6. A oferta está disponível no país e para aquela conta?

## Escolhendo uma oferta de assinatura

Uma assinatura pode ter mais de um plano-base e várias ofertas. `ProductDetails.subscriptionOfferDetails` contém tokens de oferta; o checkout precisa enviar o token escolhido.

Não pegue cegamente `first()`. Crie uma regra explícita por tag, plano ou período:

```kotlin
fun ProductDetails.findOfferToken(tag: String): String? {
    return subscriptionOfferDetails
        ?.firstOrNull { offer -> tag in offer.offerTags }
        ?.offerToken
}
```

A seleção também deve respeitar elegibilidade devolvida pela Play. Se a oferta introdutória não vier para aquela conta, mostre o plano normal. Nunca prometa “7 dias grátis” com texto fixo quando a Play não retornou a fase gratuita.

Para exibir o preço, percorra `pricingPhases.pricingPhaseList`. Uma oferta pode ter fase gratuita, preço promocional e preço recorrente. A tela deve deixar claro o que é cobrado agora e depois.

## Abrindo o checkout

O checkout precisa de uma `Activity` visível. Para compra única:

```kotlin
fun launchOneTimePurchase(
    activity: Activity,
    details: ProductDetails,
) {
    val productParams = BillingFlowParams.ProductDetailsParams.newBuilder()
        .setProductDetails(details)
        .build()

    val flowParams = BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(listOf(productParams))
        .build()

    val result = billingClient.launchBillingFlow(activity, flowParams)
    if (result.responseCode != BillingClient.BillingResponseCode.OK) {
        _state.update { it.copy(error = result.debugMessage) }
    } else {
        _state.update { it.copy(purchaseInProgress = true) }
    }
}
```

Para assinatura, inclua o `offerToken`:

```kotlin
fun launchSubscription(
    activity: Activity,
    details: ProductDetails,
    offerToken: String,
) {
    val productParams = BillingFlowParams.ProductDetailsParams.newBuilder()
        .setProductDetails(details)
        .setOfferToken(offerToken)
        .build()

    val params = BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(listOf(productParams))
        .build()

    billingClient.launchBillingFlow(activity, params)
}
```

O retorno imediato de `launchBillingFlow` só diz se a Play conseguiu **abrir** o fluxo. O resultado final chega em `onPurchasesUpdated` ou em uma consulta posterior.

## Recebendo e processando compras

Trate todos os códigos relevantes e nunca conceda o benefício só porque uma `Purchase` existe:

```kotlin
override fun onPurchasesUpdated(
    billingResult: BillingResult,
    purchases: MutableList<Purchase>?,
) {
    when (billingResult.responseCode) {
        BillingClient.BillingResponseCode.OK -> {
            purchases.orEmpty().forEach { purchase ->
                appScope.launch { processPurchase(purchase) }
            }
        }

        BillingClient.BillingResponseCode.USER_CANCELED -> {
            _state.update {
                it.copy(purchaseInProgress = false, message = "Compra cancelada.")
            }
        }

        BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
            appScope.launch { restorePurchases() }
        }

        else -> {
            _state.update {
                it.copy(
                    purchaseInProgress = false,
                    error = billingResult.debugMessage.ifBlank {
                        "Não foi possível concluir a compra."
                    },
                )
            }
        }
    }
}
```

O processamento deve diferenciar `PENDING` de `PURCHASED`:

```kotlin
private suspend fun processPurchase(purchase: Purchase) {
    when (purchase.purchaseState) {
        Purchase.PurchaseState.PENDING -> {
            _state.update {
                it.copy(
                    purchaseInProgress = false,
                    message = "Pagamento pendente. O acesso será liberado após a confirmação.",
                )
            }
        }

        Purchase.PurchaseState.PURCHASED -> {
            val verification = backend.verifyPurchase(
                purchaseToken = purchase.purchaseToken,
                products = purchase.products,
            )

            if (!verification.valid) {
                _state.update {
                    it.copy(purchaseInProgress = false, error = "Compra não validada.")
                }
                return
            }

            if (!purchase.isAcknowledged) {
                acknowledge(purchase.purchaseToken)
            }

            _state.update {
                it.copy(
                    premiumActive = verification.premiumActive,
                    purchaseInProgress = false,
                    message = "Compra confirmada.",
                )
            }
        }
    }
}
```

**Compra pendente não libera conteúdo.** Alguns meios de pagamento levam tempo para concluir. A UI deve explicar que o pedido está em análise e restaurar o direito quando o estado mudar para comprado.

## Confirmar ou consumir?

Após validar e entregar o benefício, uma compra precisa ser **confirmada** ou **consumida**, conforme o produto.

Produto não consumível e assinatura usam confirmação:

```kotlin
private suspend fun acknowledge(purchaseToken: String) {
    val params = AcknowledgePurchaseParams.newBuilder()
        .setPurchaseToken(purchaseToken)
        .build()

    val result = billingClient.acknowledgePurchase(params)
    check(result.responseCode == BillingClient.BillingResponseCode.OK) {
        result.debugMessage
    }
}
```

Produto consumível usa `consumeAsync`/a operação suspensa equivalente da versão adotada. O momento correto é depois de o backend registrar a concessão de créditos de forma **idempotente**. Se o app consome primeiro e falha antes de registrar, o usuário paga e não recebe. Se concede duas vezes ao repetir uma requisição, você cria um problema financeiro.

Idempotência significa que processar o mesmo `purchaseToken` novamente produz o mesmo resultado, sem duplicar benefício.

## Validação no backend

A validação local não é suficiente para um produto com valor real. O APK pode ser modificado, respostas podem ser simuladas e estado local pode ser alterado. O backend deve:

1. autenticar o usuário do seu app;
2. receber `purchaseToken`, package name e produto esperado;
3. consultar a API de desenvolvedor da Google Play com credencial de serviço guardada no servidor;
4. verificar produto, estado, conta vinculada e contexto da compra;
5. persistir o token com restrição de unicidade;
6. conceder ou atualizar entitlement em transação;
7. devolver ao app o estado atual, não apenas “HTTP 200”.

Nunca coloque credencial de serviço da Play dentro do APK. O aplicativo envia o token ao seu backend; o backend conversa com a API da Google.

Exemplo de contrato Ktor simplificado:

```kotlin
@Serializable
data class VerifyPurchaseRequest(
    val purchaseToken: String,
    val productIds: List<String>,
)

@Serializable
data class EntitlementResponse(
    val valid: Boolean,
    val premiumActive: Boolean,
    val reason: String? = null,
)

post("/billing/google-play/verify") {
    val user = call.requireAuthenticatedUser()
    val request = call.receive<VerifyPurchaseRequest>()

    val entitlement = billingService.verifyAndGrant(
        userId = user.id,
        token = request.purchaseToken,
        expectedProducts = request.productIds,
    )

    call.respond(entitlement)
}
```

Se o seu backend usa Kotlin, o guia de [APIs REST com Ktor](/blog/ktor-criando-apis-kotlin/) e o de [autenticação JWT no Ktor](/blog/ktor-authentication-jwt-kotlin-2026/) ajudam a estruturar esse endpoint. Use [kotlinx.serialization](/blog/kotlin-serialization-avancada-polimorfismo-2026/) para contratos, mas nunca registre o token completo em logs.

## Assinaturas exigem eventos do servidor

Consultar a compra só quando o usuário abre a tela não cobre o ciclo completo. Cancelamentos, renovações, reembolsos e mudanças podem ocorrer sem o app estar aberto. Uma arquitetura de assinatura deve integrar notificações em tempo real da Google Play e, ao receber um evento, consultar a API oficial antes de alterar o entitlement.

O evento é um **sinal para reconsultar**, não a fonte final de verdade. O backend precisa lidar com entrega duplicada e fora de ordem. Guarde identificadores, datas e a última versão conhecida do estado.

Para a UI, pense em estados de domínio:

- `ACTIVE`: acesso normal;
- `PENDING`: pagamento ainda não concluído;
- `GRACE_PERIOD`: comunique problema de pagamento sem cortar cedo demais;
- `ON_HOLD` ou equivalente: acesso conforme regra de negócio e estado oficial;
- `CANCELED_BUT_ACTIVE`: cancelada, porém válida até o fim do período;
- `EXPIRED`: benefício encerrado;
- `REVOKED`: compra anulada/reembolsada conforme confirmação do servidor.

Não invente esses estados a partir de um boolean local. Derive o entitlement da resposta validada e das regras atuais da Play.

## Restaurando compras

A restauração é necessária quando o app reinstala, troca de aparelho, perde o cache ou recebe atualização de assinatura. Consulte compras atuais ao conectar e quando a Activity volta ao primeiro plano após um checkout externo:

```kotlin
private suspend fun restorePurchases() {
    val inApp = billingClient.queryPurchasesAsync(
        QueryPurchasesParams.newBuilder()
            .setProductType(BillingClient.ProductType.INAPP)
            .build()
    )

    val subs = billingClient.queryPurchasesAsync(
        QueryPurchasesParams.newBuilder()
            .setProductType(BillingClient.ProductType.SUBS)
            .build()
    )

    val purchases = inApp.purchasesList + subs.purchasesList
    purchases.forEach { processPurchase(it) }

    // A fonte final para "premium ativo" deve continuar sendo o backend.
    val entitlement = backend.getCurrentEntitlement()
    _state.update { it.copy(premiumActive = entitlement.premiumActive) }
}
```

Ofereça um botão “Restaurar compras” para suporte e transparência, mas a restauração também deve ocorrer automaticamente. O botão não pode ser o único caminho para recuperar acesso pago.

## Integrando com Jetpack Compose

O ViewModel expõe estado e métodos sem entregar o `BillingClient` à tela:

```kotlin
class PaywallViewModel(
    private val billing: PlayBillingRepository,
) : ViewModel() {

    val state = billing.state.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = BillingUiState(),
    )

    fun connect() = billing.connect()
    fun restore() = viewModelScope.launch { billing.restoreFromUserAction() }
}
```

Tela resumida:

```kotlin
@Composable
fun PaywallScreen(
    viewModel: PaywallViewModel,
    onBuy: (ProductDetails) -> Unit,
) {
    val state by viewModel.state.collectAsStateWithLifecycle()

    LaunchedEffect(Unit) {
        viewModel.connect()
    }

    when {
        state.loading -> CircularProgressIndicator()
        state.premiumActive -> Text("Seu plano Pro está ativo.")
        else -> Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
            Text("Escolha seu plano", style = MaterialTheme.typography.headlineMedium)

            state.products.forEach { product ->
                ProductCard(
                    title = product.name,
                    description = product.description,
                    price = product.displayPrice(),
                    enabled = !state.purchaseInProgress,
                    onClick = { onBuy(product) },
                )
            }

            TextButton(onClick = viewModel::restore) {
                Text("Restaurar compras")
            }

            state.message?.let { Text(it) }
            state.error?.let {
                Text(it, color = MaterialTheme.colorScheme.error)
            }
        }
    }
}
```

A Activity pode converter o evento da UI em `launchBillingFlow`, porque a API exige uma Activity. Em projetos com Navigation Compose, passe um callback da rota hospedeira. Não guarde uma Activity no ViewModel ou repository singleton: isso vaza contexto e mistura UI com infraestrutura.

## Segurança e privacidade

Uma implementação de pagamento coleta identificadores sensíveis, mas não precisa registrar tudo. Práticas essenciais:

- envie token somente por HTTPS;
- não grave token completo em analytics, Crashlytics ou logs;
- associe compra ao usuário autenticado e, quando aplicável, use identificadores ofuscados suportados pelo fluxo;
- valide package, produto e token no servidor;
- armazene o mínimo necessário para auditoria e idempotência;
- não confie em `SharedPreferences`/DataStore para autorização premium;
- use biometria apenas para confirmar uma ação local sensível, nunca como prova de pagamento;
- trate replay: o mesmo token não pode conceder benefício a duas contas.

Para apps offline-first, o backend continua sendo a autoridade. Você pode manter um cache de entitlement com validade curta para não bloquear a UI durante uma queda de rede, seguindo o desenho de [Android offline-first](/blog/android-offline-first-kotlin-2026/), mas precisa definir quando o cache expira e como evitar acesso indefinido após revogação.

## Testando sem cobrar de verdade

Não valide Billing apenas com um APK instalado via `adb`. Use as ferramentas de teste da Play:

1. configure testadores de licença no Play Console;
2. publique em teste interno ou faixa apropriada;
3. instale a build pela Play com a conta testadora;
4. teste sucesso, cancelamento, pendência, item já possuído e indisponibilidade;
5. confira confirmação/consumo e chamadas do backend;
6. repita em conta sem oferta introdutória elegível;
7. teste reinstalação e restauração;
8. simule retry do endpoint para provar idempotência.

Tenha fakes para teste unitário. `BillingClient` é infraestrutura Android e não deve dominar o teste do ViewModel:

```kotlin
interface BillingGateway {
    val state: StateFlow<BillingUiState>
    fun connect()
    suspend fun restore()
}

class FakeBillingGateway : BillingGateway {
    override val state = MutableStateFlow(
        BillingUiState(
            loading = false,
            connected = true,
            premiumActive = false,
        )
    )

    override fun connect() = Unit
    override suspend fun restore() = Unit
}
```

Nos testes de UI, valide estados da paywall, loading, erro e premium ativo. O checkout real fica para teste integrado em faixa da Play. O guia de [testes Android com Compose e Maestro](/guias/testes-android-compose-maestro/) complementa o fluxo visual.

## Erros comuns

1. **Liberar premium no clique do botão.** O clique só inicia o checkout.
2. **Confiar no callback sem backend.** Um cliente modificado pode falsificar o caminho local.
3. **Ignorar `PENDING`.** Pagamento pendente não equivale a compra concluída.
4. **Não confirmar a compra.** A Play pode reverter compras não reconhecidas dentro da janela prevista pela política.
5. **Consumir antes de conceder créditos.** Uma falha intermediária deixa o usuário sem compra e sem benefício.
6. **Conceder novamente em todo retry.** Falta de idempotência duplica créditos.
7. **Usar `first()` em ofertas.** A primeira oferta não é necessariamente a desejada ou elegível.
8. **Fixar preço no layout.** O valor localizado deve vir de `ProductDetails`.
9. **Não restaurar na inicialização.** Reinstalação parece “perder” a compra.
10. **Guardar Activity em singleton.** Cria vazamento e dependência de lifecycle.
11. **Logar token de compra.** Transforma observabilidade em risco de segurança.
12. **Tratar cancelamento como expiração imediata.** Assinatura cancelada pode continuar ativa até o fim do período pago.

## Checklist de produção

- Produtos, planos e ofertas ativos no Play Console.
- Billing Library na versão estável aceita pela política atual da loja.
- `BillingClient` único por escopo e reconexão com backoff.
- Catálogo consultado via `ProductDetails`; preços não hardcoded.
- `PURCHASED` e `PENDING` tratados separadamente.
- Token validado no backend antes da concessão definitiva.
- Endpoint idempotente e token único por compra.
- `acknowledgePurchase` ou consumo no momento correto.
- Compras restauradas automaticamente e por ação manual.
- Assinaturas sincronizadas por eventos de servidor + consulta oficial.
- Tokens ausentes de logs e analytics.
- Testes internos cobrindo sucesso, cancelamento, pendência e reinstalação.
- Runbook de suporte para “paguei e não liberou”.

## Perguntas frequentes

### Posso liberar premium apenas com a validação no celular?

Para protótipo descartável até pode parecer suficiente, mas não é uma arquitetura segura para produção. O backend deve validar o token na Google Play, registrar a compra de modo idempotente e responder com o entitlement atual.

### Quando devo chamar acknowledgePurchase?

Depois de confirmar que a compra está em estado comprado, validar o token e conceder o benefício com sucesso. Para consumíveis, use o fluxo de consumo depois de registrar a concessão. Não confirme uma compra pendente.

### Como mostrar o preço da assinatura no Compose?

Use as fases de preço em `subscriptionOfferDetails` para a oferta selecionada. Mostre preço, período, fase grátis/promocional e cobrança recorrente de maneira clara. Não escreva valores fixos no composable.

### O usuário cancelou. Devo remover o acesso na hora?

Normalmente, não. Uma assinatura cancelada pode continuar válida até o fim do período já pago. O backend deve consultar o estado oficial e calcular o entitlement; “cancelada” e “expirada” são situações diferentes.

### Preciso de um botão Restaurar compras no Android?

É uma boa prática de suporte e transparência, mas o app também deve restaurar automaticamente ao conectar e consultar o backend. O usuário não deveria descobrir sozinho como recuperar um direito pago.

### Google Play Billing serve para vender produto físico?

O Billing é voltado a produtos e recursos digitais no app. Produtos físicos e serviços consumidos fora do app seguem outro fluxo de pagamento e as políticas aplicáveis. Confirme a categoria atual nas políticas da Google Play antes de implementar.

## Conclusão

**Google Play Billing com Kotlin e Jetpack Compose** fica confiável quando você separa UI, cliente de cobrança e autoridade de acesso. Compose apresenta o catálogo e inicia o checkout; `BillingClient` entrega detalhes e compras; o backend valida tokens, mantém idempotência e acompanha o ciclo de vida; o app restaura o entitlement sempre que necessário.

O principal aprendizado é simples: não modele “premium” como um boolean salvo no aparelho. Modele uma compra verificável e um direito de acesso derivado dela. Essa decisão evita fraude, compras perdidas, crédito duplicado e suporte manual quando uma assinatura muda fora do app.

Para continuar, revise [arquitetura MVVM](/guias/guia-arquitetura-mvvm-kotlin/), [segurança local no Android](/blog/seguranca-dados-locais-android-kotlin-2026/), [App Links](/blog/app-links-deep-links-android-kotlin-2026/) para retornos controlados e [WorkManager](/blog/workmanager-kotlin-android-2026/) para sincronizações que não dependem de uma tela aberta. Quem busca trabalhar com monetização mobile também pode acompanhar as [vagas Kotlin e Android](/vagas/) do Kotlin Brasil.
