No post anterior, preparamos a base — constantes, enum de tipos e modelo de dados. Agora é hora de sujar as mãos com código nativo. Vamos configurar o Android para capturar deep links e entregá-los ao Flutter — sem nenhum pacote externo no meio do caminho.
Spoiler: é mais simples do que parece — mas alguns detalhes fazem toda a diferença na prática.
Dando continuidade à série sobre Deep Links no Flutter, com novos artigos saindo semanalmente, ou quase. Se você ainda não viu a base do projeto em Flutter, recomendo começar pelo primeiro artigo da série: Post 1 — Guia para Iniciantes.
Neste artigo, você vai aprender:
- Como configurar o AndroidManifest.xml para deep links.
- A diferença entre MethodChannel e EventChannel.
- Como testar com adb sem precisar de um dispositivo físico.
Configurando o AndroidManifest.xml
O Android descobre que seu app pode tratar um link por meio de intent-filters declarados no AndroidManifest.xml. Podemos declarar dois intent-filters separados (um para custom scheme e outro para HTTPS) ou um único filtro com múltiplos <data>. Neste exemplo, vamos manter separados para maior clareza didática.
<!-- android/app/src/main/AndroidManifest.xml -->
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
...>
<!-- Intent filter padrão (launcher) -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Deep Link: Custom Scheme -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="fitconnect" />
<data android:host="fitconnect.app" />
</intent-filter>
<!-- Deep Link: HTTPS (App Links) -->
<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" />
<data android:host="fitconnect.app" />
<data android:pathPrefix="/signup" />
</intent-filter>
</activity>
Três atributos merecem atenção:
-
android:launchMode="singleTop": evita criar uma nova instância da Activity quando ela já está no topo da stack, direcionando o novo intent paraonNewIntent. Sem isso, o usuário poderia acabar com múltiplas instâncias da mesma Activity na stack. -
BROWSABLE: autoriza que links externos — de um e-mail, mensagem ou navegador — possam abrir o app. Sem essa categoria, o intent-filter é invisível para o sistema. -
android:autoVerify="true": ativa a verificação bidirecional do App Links. O Android vai checar se o servidor confirma que este app tem permissão para tratar o domínio. Montamos esse arquivo de verificação no Post 5.
Implementando o MainActivity.kt
Com o manifest configurado, o Android vai entregar o link como um Intent para a MainActivity. Agora precisamos capturá-lo e repassar ao Flutter via dois canais:
-
MethodChannel: Flutter chama quando o app abre e precisa saber se havia um link pendente. -
EventChannel: stream contínuo para links recebidos com o app já em execução.
package com.fitconnect.app
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterFragmentActivity() {
private val TAG = "FitConnect"
private val CHANNEL = "com.fitconnect.app/deeplink"
private val EVENT_CHANNEL = "com.fitconnect.app/deeplink_stream"
private var methodChannel: MethodChannel? = null
private var eventChannel: EventChannel? = null
private var eventSink: EventChannel.EventSink? = null
private var initialLink: String? = null
private var lastProcessedLink: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate - checking for deep link")
handleIntent(intent, isInitial = true)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// MethodChannel: Flutter pede deep link inicial
methodChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL
)
methodChannel?.setMethodCallHandler { call, result ->
when (call.method) {
"getInitialLink" -> result.success(initialLink)
else -> result.notImplemented()
}
}
// EventChannel: Stream de deep links em tempo real
eventChannel = EventChannel(
flutterEngine.dartExecutor.binaryMessenger,
EVENT_CHANNEL
)
eventChannel?.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(args: Any?, events: EventChannel.EventSink?) {
eventSink = events
}
override fun onCancel(args: Any?) {
eventSink = null
}
})
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Log.d(TAG, "onNewIntent - app was already open")
handleIntent(intent, isInitial = false)
}
private fun handleIntent(intent: Intent?, isInitial: Boolean) {
val data: Uri? = intent?.data
if (intent?.action == Intent.ACTION_VIEW && data != null) {
val deepLinkUrl = data.toString()
// Evita processar o mesmo link duas vezes
if (deepLinkUrl == lastProcessedLink) return
lastProcessedLink = deepLinkUrl
Log.i(TAG, "Deep link: $deepLinkUrl")
if (isInitial) {
// App estava fechado
initialLink = deepLinkUrl
} else {
// App estava aberto — envia via stream
eventSink?.success(deepLinkUrl)
}
}
}
}
O que acontece em cada cenário
O fluxo de entrega do link depende de como o app foi aberto:
O lastProcessedLink existe para um caso específico: em algumas versões do Android, onCreate e onNewIntent podem ser chamados em sequência para o mesmo link. Sem essa proteção, o Flutter processaria o mesmo deep link duas vezes.
Testando com adb
Antes de integrar com o Flutter, vale confirmar que o Android está capturando os links corretamente. O adb permite simular cliques em links diretamente pelo terminal.
# Testa o custom scheme (fitconnect://)
# Deve abrir o app sem precisar de verificação de domínio
adb shell am start -a android.intent.action.VIEW \
-d "fitconnect://fitconnect.app/signup?referralCode=TRAINER1234567890123"
# Testa o App Link (HTTPS)
# Pode abrir um seletor de aplicativos se a verificação do domínio ainda não estiver concluída
adb shell am start -a android.intent.action.VIEW \
-d "https://fitconnect.app/signup?referralCode=TRAINER1234567890123"
Se o custom scheme abrir o app, o manifest está correto. Se o HTTPS mostrar um seletor de aplicativos em vez de abrir direto — é sinal de que a verificação bidirecional ainda não está configurada. Isso é esperado neste ponto da série e será resolvido no Post 5.
O que construímos até aqui
Ao final desta etapa, você já tem:
- O AndroidManifest configurado com os dois intent-filters: custom scheme e App Links.
- O
MainActivity.ktcomMethodChannel(link inicial) eEventChannel(stream em tempo real). - Clareza sobre a diferença entre
onCreateeonNewIntente por que ambos precisam ser tratados. - Comandos
adbprontos para testar sem precisar de dispositivo físico.
Este é o segundo de 9 posts da série. Se você já passou por alguma situação estranha com intent-filters no Android, conta nos comentários — esse tipo de detalhe vai direto para os próximos artigos.
Na próxima etapa, vamos replicar esse fluxo no iOS usando Swift — e você vai perceber como os conceitos são os mesmos, mesmo com APIs diferentes.
Código completo disponível no repositório: FitConnect no GitHub
Se esse conteúdo te ajudou, deixa um clap 👏 e salva o post — isso me ajuda a continuar a série.
E se você já implementou deep links no Android, comenta aqui qual foi o maior desafio — vou usar isso nos próximos artigos.
Tags: Flutter, Android, Kotlin, Deep Links, Mobile Development


Top comments (0)