---
title: "App Links e Deep Links no Android com Kotlin | Kotlin Brasil"
url: "https://kotlin.dev.br/blog/app-links-deep-links-android-kotlin-2026/"
markdown_url: "https://kotlin.dev.br/blog/app-links-deep-links-android-kotlin-2026.MD"
description: "Aprenda a implementar App Links e deep links em apps Android com Kotlin, Navigation Compose, testes, analytics e cuidados de segurança para produção."
date: "2026-06-01"
author: "Karina Melo"
---

# App Links e Deep Links no Android com Kotlin | Kotlin Brasil

Aprenda a implementar App Links e deep links em apps Android com Kotlin, Navigation Compose, testes, analytics e cuidados de segurança para produção.


Deep links parecem detalhe de produto até o primeiro incidente real: uma campanha abre a tela errada, um link de recuperação de conta cai na home, uma notificação perde o contexto, ou uma URL pública aceita parâmetros sem validação. Em apps Android com Kotlin, **App Links e deep links** conectam marketing, onboarding, notificações, login, checkout, suporte e retenção. Por isso, eles precisam ser tratados como parte da arquitetura do app, não como uma regra improvisada dentro da `Activity`.

Um deep link é qualquer link que leva a uma área específica do app. Um App Link é um caso mais confiável: uma URL HTTPS associada ao domínio do produto, verificada pelo Android por meio do arquivo `assetlinks.json`. Na prática, App Links reduzem a fricção porque o sistema pode abrir o app diretamente sem mostrar seletor, enquanto deep links customizados como `meuapp://pedido/123` continuam úteis para integrações internas, ambientes de teste ou fluxos onde HTTPS não é possível.

Este guia mostra como desenhar App Links e deep links em um app Android moderno com Kotlin, Compose e Navigation. A ideia é cobrir produção: contrato de URLs, manifesto, navegação tipada, validação de parâmetros, testes, analytics e segurança. Se você ainda está montando a base Android, combine este conteúdo com [Kotlin para Android](/blog/kotlin-para-android/), [Navigation 3 com Compose](/blog/navigation-3-compose-android-2026/), [testes Android com Compose e Maestro](/guias/testes-android-compose-maestro/) e [Firebase Crashlytics com Kotlin](/blog/firebase-crashlytics-anr-android-kotlin-2026/).

## Quando usar App Links

Use App Links quando a URL representa uma rota real do seu produto e pode ser compartilhada fora do app. Alguns exemplos comuns:

- abrir um produto, artigo, pedido, conversa ou perfil específico;
- retomar onboarding depois de confirmação por e-mail;
- levar uma pessoa de uma notificação para a tela exata do evento;
- abrir uma promoção sem perder o identificador da campanha;
- direcionar suporte para uma área já autenticada do app;
- permitir que o site e o app compartilhem a mesma URL pública.

O ganho principal é consistência. Se `https://exemplo.com/pedidos/123` funciona no navegador, o app pode tratar essa mesma URL como entrada para a tela de detalhe do pedido. Isso melhora SEO, campanhas, compartilhamento e manutenção, porque o time não precisa inventar contratos paralelos para web, mobile e notificações.

## Desenhe o contrato de URLs antes do código

O erro mais comum é começar pelo `AndroidManifest.xml` e decidir as rotas depois. Faça o contrário. Liste os caminhos que o app realmente aceita, quem os gera e quais parâmetros são obrigatórios.

Um contrato simples pode ficar assim:

| URL | Tela | Parâmetros | Observação |
| --- | --- | --- | --- |
| `/artigos/{slug}` | detalhe de artigo | `slug` | público |
| `/vagas/{id}` | detalhe de vaga | `id` | público |
| `/conta/confirmar` | confirmação | `token` | sensível |
| `/checkout/{cartId}` | checkout | `cartId`, `utm_source` | exige login |

Evite URLs que dependem de estado escondido, como `/abrir?id=123&tipo=pedido&acao=detalhe`. Esse formato parece flexível, mas vira uma API sem contrato. Prefira caminhos explícitos e versionáveis.

## Manifesto Android para App Links

No Android, você declara os links aceitos dentro da `Activity` que recebe a entrada. Para App Links HTTPS, use `android:autoVerify="true"`:

```xml
<activity
    android:name=".MainActivity"
    android:exported="true">

    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data
            android:scheme="https"
            android:host="exemplo.com"
            android:pathPrefix="/artigos" />
    </intent-filter>

    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data
            android:scheme="https"
            android:host="exemplo.com"
            android:pathPrefix="/vagas" />
    </intent-filter>
</activity>
```

Separe filtros quando os caminhos têm comportamentos diferentes. Isso facilita auditoria, testes e remoção futura de rotas antigas. Também evite declarar um `pathPrefix="/"` amplo demais se o app só sabe tratar algumas URLs. Quanto mais aberto o filtro, maior o risco de abrir telas inesperadas.

## Arquivo `assetlinks.json`

Para o Android verificar que o domínio pertence ao app, publique um arquivo em:

```text
https://exemplo.com/.well-known/assetlinks.json
```

O conteúdo aponta para o pacote Android e o SHA-256 do certificado usado na assinatura:

```json
[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "br.dev.exemplo",
      "sha256_cert_fingerprints": [
        "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
      ]
    }
  }
]
```

Em times com vários ambientes, documente quais domínios usam certificado de debug, staging e produção. Um App Link que funciona localmente pode falhar em produção se o SHA-256 publicado não corresponder à chave final da Play Store.

## Convertendo URL em destino de navegação

Não deixe parsing de URL espalhado em Composables. Crie uma pequena fronteira que transforma `Uri` em uma intenção de navegação tipada:

```kotlin
sealed interface DeepLinkDestination {
    data class Artigo(val slug: String) : DeepLinkDestination
    data class Vaga(val id: String) : DeepLinkDestination
    data class ConfirmarConta(val token: String) : DeepLinkDestination
    data object Home : DeepLinkDestination
}

fun parseDeepLink(uri: Uri): DeepLinkDestination {
    val segments = uri.pathSegments

    return when {
        segments.size == 2 && segments[0] == "artigos" -> {
            DeepLinkDestination.Artigo(slug = segments[1])
        }

        segments.size == 2 && segments[0] == "vagas" -> {
            DeepLinkDestination.Vaga(id = segments[1])
        }

        segments == listOf("conta", "confirmar") -> {
            val token = uri.getQueryParameter("token").orEmpty()
            DeepLinkDestination.ConfirmarConta(token = token)
        }

        else -> DeepLinkDestination.Home
    }
}
```

Essa função é simples de testar na JVM. Ela também força o time a decidir o que acontece com URL inválida. Para conteúdo público, cair na home pode ser aceitável. Para confirmação de conta ou checkout, é melhor mostrar uma tela de erro controlada do que tentar seguir com dados incompletos.

## Integração com Compose e Navigation

Na `MainActivity`, leia o `Intent` inicial e também trate novos intents quando a Activity já estiver aberta:

```kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val initialUri = intent?.data

        setContent {
            AppRoot(initialDeepLink = initialUri)
        }
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        // Encaminhe intent.data para o estado de navegação do app.
    }
}
```

Em apps pequenos, você pode chamar `navController.navigate(...)` depois de parsear a URL. Em apps maiores, prefira converter o link em evento de aplicação e deixar a camada de navegação decidir o destino. Isso combina melhor com `StateFlow`, `ViewModel` e navegação orientada a estado.

Um exemplo direto com Navigation Compose tradicional:

```kotlin
fun NavController.openDeepLink(destination: DeepLinkDestination) {
    when (destination) {
        is DeepLinkDestination.Artigo -> navigate("artigos/${destination.slug}")
        is DeepLinkDestination.Vaga -> navigate("vagas/${destination.id}")
        is DeepLinkDestination.ConfirmarConta -> navigate("conta/confirmar")
        DeepLinkDestination.Home -> navigate("home")
    }
}
```

Se você estiver usando Navigation 3, a mesma ideia vale, mas o destino pode ser uma chave serializável em vez de uma string. Isso reduz erro de rota e facilita restauração de estado.

## Segurança e validação

Deep link é entrada externa. Trate como input não confiável.

Valide host, caminho e parâmetros antes de navegar. Nunca execute ação destrutiva apenas porque uma URL chegou ao app. Uma rota como `/conta/excluir?confirm=true` seria perigosa se abrisse uma ação irreversível sem tela de confirmação. Também evite colocar dados sensíveis em query string, porque URLs podem aparecer em logs, analytics, screenshots e histórico do navegador.

Para fluxos autenticados, o link deve levar até uma tela que verifica sessão. Se o usuário não estiver logado, salve o destino pendente de forma segura, faça login e só depois continue. Para tokens de confirmação, use validade curta, uso único e tratamento claro para token expirado.

## Analytics sem vazar dados

Instrumente eventos de deep link para saber se campanhas e notificações funcionam, mas não envie o token nem identificadores sensíveis. Um evento útil pode conter:

- rota normalizada, como `/vagas/{id}`;
- origem, como push, e-mail, web ou campanha;
- resultado, como aberto, inválido, exige login ou expirado;
- versão do app e plataforma.

Evite registrar a URL completa quando ela contém `token`, `email`, `cpf`, `session`, `cartId` ou qualquer dado pessoal. Para estabilidade, conecte falhas de parsing e URLs inesperadas ao seu processo de observabilidade, como explicado no guia de [Crashlytics com Kotlin](/blog/firebase-crashlytics-anr-android-kotlin-2026/).

## Testes que realmente pegam regressão

Comece com testes unitários para `parseDeepLink`. Eles são baratos e pegam a maioria dos erros de contrato:

```kotlin
@Test
fun `abre artigo por slug`() {
    val uri = "https://exemplo.com/artigos/navigation-3".toUri()

    val destination = parseDeepLink(uri)

    assertEquals(
        DeepLinkDestination.Artigo("navigation-3"),
        destination,
    )
}
```

Depois, adicione um teste instrumentado ou E2E para uma jornada crítica. Com ADB, é possível disparar uma intent real:

```bash
adb shell am start \
  -a android.intent.action.VIEW \
  -d "https://exemplo.com/vagas/123" \
  br.dev.exemplo
```

Em Maestro, o fluxo pode abrir o link e verificar a tela esperada. Não automatize todas as rotas. Escolha login, recuperação de conta, checkout, detalhe público e uma rota inválida. Essa seleção cobre risco real sem criar uma suíte lenta.

## Checklist de produção

Antes de considerar App Links prontos para produção, revise:

- contrato de URL documentado e aprovado por web, mobile e produto;
- `AndroidManifest.xml` sem filtros amplos demais;
- `assetlinks.json` publicado no domínio correto com SHA-256 de produção;
- parsing de URL coberto por testes unitários;
- rotas sensíveis exigem autenticação e confirmação explícita;
- analytics usa rota normalizada, não URL completa com dados sensíveis;
- links de campanha, push e e-mail testados em dispositivo real;
- comportamento definido para app instalado, app não instalado e usuário deslogado.

App Links bem feitos não são apenas conveniência. Eles conectam aquisição, retenção, suporte e arquitetura. Para quem trabalha com Android Kotlin em produto real, dominar esse fluxo demonstra maturidade: você entende navegação, segurança, analytics, testes e experiência do usuário como partes do mesmo sistema.
