Resposta rápida: para mostrar o Google Maps no Jetpack Compose com Kotlin, habilite o Maps SDK for Android no projeto do Google Cloud, restrinja a API key ao aplicativo Android, adicione as dependências oficiais e renderize um GoogleMap dentro de um Composable. Controle enquadramento com CameraPositionState, represente locais com Marker ou MarkerInfoWindow e mantenha os pontos exibidos no estado do ViewModel. Solicite localização somente se a funcionalidade realmente precisar da posição do aparelho; um mapa com endereços, lojas ou rotas planejadas pode funcionar sem essa permissão. Quando usar localização, obtenha coordenadas em uma camada separada, trate ausência de sinal e nunca confunda “última posição conhecida” com uma leitura atual.

Mapas aparecem em apps de entrega, mobilidade, turismo, eventos, imóveis, logística e busca de lojas. A primeira implementação costuma parecer simples: colocar o mapa na tela e adicionar um pin. Em produção, porém, surgem perguntas mais importantes: onde guardar a chave, quem controla a câmera, como evitar recomposições caras, o que fazer sem Google Play Services, como agrupar muitos pontos e quanto dado de localização deve sair do aparelho.

Este guia apresenta uma base moderna para Maps Compose, com configuração segura, mapa inicial, marcadores, eventos, posição do usuário, arquitetura, desempenho, testes e cuidados de privacidade. As APIs e os requisitos comerciais podem evoluir; confirme versões, faturamento e configuração na documentação oficial do Maps SDK for Android e na referência da biblioteca Maps Compose antes de publicar.

Como a integração é dividida

Uma tela de mapa normalmente combina componentes com responsabilidades diferentes:

ComponenteResponsabilidade
Maps SDK for Androidrenderização do mapa e serviços da plataforma
Maps ComposeComposables e estado para integrar o mapa à UI Compose
ViewModelestado de tela, seleção, filtros e carregamento dos pontos
Repositóriobusca de lojas, entregas, imóveis ou outros itens do domínio
Provedor de localizaçãocoordenadas do aparelho, quando necessárias
Google Cloud Consoleativação da API, credenciais, restrições e cotas

GoogleMap deve renderizar o estado; ele não precisa consultar sua API REST nem decidir regras de negócio. Um marcador também não deve carregar um objeto gigante da camada de rede. Prefira transformar a resposta em um modelo pequeno e estável para a tela.

data class MapPoint(
    val id: String,
    val title: String,
    val subtitle: String?,
    val latitude: Double,
    val longitude: Double,
)

data class MapUiState(
    val points: List<MapPoint> = emptyList(),
    val selectedPointId: String? = null,
    val isLoading: Boolean = false,
    val message: String? = null,
)

Essa separação facilita testes, evita que detalhes do SDK vazem pelo domínio e segue os mesmos princípios do guia de arquitetura MVVM com Kotlin.

Configure o projeto no Google Cloud

Antes do código Android, crie ou selecione um projeto no Google Cloud e habilite o Maps SDK for Android. Dependendo dos serviços utilizados — mapa, Places, Routes ou geocodificação — outras APIs podem ser necessárias. Ative somente o que o produto usa e acompanhe as regras atuais de cobrança e cota.

Depois, crie uma API key e aplique duas restrições:

  1. restrição de aplicativo: Android apps;
  2. restrição de API: apenas as APIs necessárias para esse cliente.

Na restrição Android, associe o nome do pacote e a impressão digital SHA-1 do certificado. Cadastros de debug e release geralmente têm impressões diferentes. Se o app usa Play App Signing, confira também o certificado de assinatura gerenciado pelo Google Play; usar apenas a chave local é uma causa comum de mapa funcionar no debug e falhar após a publicação.

Uma API key de Maps usada pelo cliente Android não é um segredo capaz de permanecer invisível dentro do APK. A proteção prática vem das restrições corretas, da redução das APIs permitidas, do monitoramento de consumo e da rotação quando houver abuso. Não dependa apenas de ofuscação.

Importante: não coloque credenciais de backend, tokens de usuário ou chaves sem restrição no manifesto. O cliente móvel é um ambiente não confiável.

Injete a API key sem versioná-la

Uma abordagem comum é manter o valor local fora do Git e usar o Secrets Gradle Plugin for Android para gerar o placeholder consumido pelo manifesto. Confira a configuração vigente na documentação de uso de API keys no Android.

No arquivo local que não será commitado:

# local.properties ou arquivo definido pelo projeto
MAPS_API_KEY=sua-chave-restrita

No manifesto, referencie o placeholder configurado pelo plugin:

<application
    android:name=".MapApplication"
    android:theme="@style/Theme.Exemplo">

    <meta-data
        android:name="com.google.android.geo.API_KEY"
        android:value="${MAPS_API_KEY}" />

</application>

Forneça um valor separado pelo ambiente de CI para builds de release. Não faça commit da chave real, não imprima o valor em logs e não reutilize a mesma credencial irrestrita em Android, iOS, web e servidor.

Para validar a restrição, instale uma build assinada como release em um dispositivo de teste. Uma configuração que funciona apenas via Android Studio ainda não está pronta para produção.

Adicione as dependências

Use as versões estáveis recomendadas nas fontes oficiais no momento da implementação. Centralizar as versões em libs.versions.toml deixa atualizações e auditorias mais simples:

[versions]
mapsCompose = "<versao-estavel-atual>"
playServicesLocation = "<versao-estavel-atual>"

[libraries]
google-maps-compose = {
  module = "com.google.maps.android:maps-compose",
  version.ref = "mapsCompose"
}
google-play-services-location = {
  module = "com.google.android.gms:play-services-location",
  version.ref = "playServicesLocation"
}
// build.gradle.kts do módulo Android
dependencies {
    implementation(libs.google.maps.compose)

    // Adicione apenas se a tela realmente consultar a posição do aparelho.
    implementation(libs.google.play.services.location)
}

O artefato de localização não é obrigatório para exibir o mapa. Essa distinção ajuda a manter permissões e dependências proporcionais ao recurso.

Se seu projeto ainda espalha números de versão em vários módulos, o guia de Gradle Version Catalog mostra uma organização mais sustentável.

Mostre o primeiro mapa em Compose

O exemplo mínimo define uma posição inicial e renderiza o mapa:

@Composable
fun StoreMapScreen() {
    val initialPosition = LatLng(-23.5505, -46.6333)
    val cameraPositionState = rememberCameraPositionState {
        position = CameraPosition.fromLatLngZoom(initialPosition, 12f)
    }

    GoogleMap(
        modifier = Modifier.fillMaxSize(),
        cameraPositionState = cameraPositionState,
    )
}

rememberCameraPositionState preserva o controlador enquanto o Composable permanece na composição. Não recrie esse estado para cada mudança irrelevante da tela, pois isso pode reposicionar o mapa e interromper o gesto do usuário.

Defina a posição inicial a partir do contexto do produto, não obrigatoriamente da localização atual. Exemplos:

  • cidade escolhida na busca;
  • área atendida pelo serviço;
  • enquadramento dos resultados retornados;
  • último viewport salvo pelo próprio app;
  • localização somente após consentimento e ação explícita.

Abrir diretamente uma solicitação de localização para centralizar um mapa que poderia começar no Brasil inteiro cria atrito sem benefício claro.

Configure propriedades e controles do mapa

Separe propriedades visuais de configurações de interação:

@Composable
fun ConfiguredMap(
    cameraPositionState: CameraPositionState,
    locationEnabled: Boolean,
) {
    val properties by remember(locationEnabled) {
        mutableStateOf(
            MapProperties(
                isMyLocationEnabled = locationEnabled,
                mapType = MapType.NORMAL,
            )
        )
    }

    val uiSettings by remember {
        mutableStateOf(
            MapUiSettings(
                zoomControlsEnabled = false,
                compassEnabled = true,
                myLocationButtonEnabled = locationEnabled,
            )
        )
    }

    GoogleMap(
        modifier = Modifier.fillMaxSize(),
        cameraPositionState = cameraPositionState,
        properties = properties,
        uiSettings = uiSettings,
    )
}

Só defina isMyLocationEnabled = true depois de confirmar que a permissão foi concedida. Ativar a camada sem autorização pode causar falha. Além disso, o ponto azul é uma representação visual controlada pelo SDK; ele não substitui uma leitura de coordenadas para regras do seu domínio.

Ao colocar controles Compose sobre o mapa, reserve espaço para barras do sistema e outros elementos:

Box(Modifier.fillMaxSize()) {
    ConfiguredMap(
        cameraPositionState = cameraPositionState,
        locationEnabled = locationEnabled,
    )

    FloatingActionButton(
        onClick = onCenterRequested,
        modifier = Modifier
            .align(Alignment.BottomEnd)
            .navigationBarsPadding()
            .padding(16.dp),
    ) {
        Icon(
            imageVector = Icons.Default.MyLocation,
            contentDescription = "Centralizar na minha localização",
        )
    }
}

Um contentDescription específico é melhor que “botão de localização”. Para mais decisões de semântica e contraste, consulte o guia de acessibilidade no Android com Compose.

Adicione marcadores a partir do estado

Para poucos pontos, renderizar um marcador por item é direto:

@Composable
fun PointsMap(
    points: List<MapPoint>,
    selectedPointId: String?,
    onPointSelected: (String) -> Unit,
) {
    val cameraPositionState = rememberCameraPositionState {
        position = CameraPosition.fromLatLngZoom(
            LatLng(-14.2350, -51.9253),
            3.5f,
        )
    }

    GoogleMap(
        modifier = Modifier.fillMaxSize(),
        cameraPositionState = cameraPositionState,
        onMapClick = { onPointSelected("") },
    ) {
        points.forEach { point ->
            val markerState = rememberUpdatedMarkerState(
                position = LatLng(point.latitude, point.longitude),
            )

            Marker(
                state = markerState,
                title = point.title,
                snippet = point.subtitle,
                alpha = if (point.id == selectedPointId) 1f else 0.85f,
                onClick = {
                    onPointSelected(point.id)
                    false
                },
            )
        }
    }
}

A API exata disponível pode variar conforme a versão do Maps Compose; ajuste rememberUpdatedMarkerState ou o construtor de estado conforme a documentação da versão adotada.

Use um ID de domínio para seleção. Não compare marcadores por título ou coordenadas: duas lojas podem ter o mesmo nome, e coordenadas podem sofrer arredondamento.

O retorno de onClick informa se seu código consumiu o evento. Retornar false permite o comportamento padrão, como abrir a info window; retornar true indica que a aplicação cuidou totalmente da ação. Escolha conscientemente para não criar um toque que seleciona o item, mas nunca mostra feedback.

Sincronize mapa e lista sem criar loops

Telas de busca costumam combinar mapa e lista. Ao tocar em um card, a câmera deve ir ao ponto; ao tocar no marcador, o card correspondente deve ficar selecionado. Centralize o ID selecionado no ViewModel:

class MapViewModel(
    private val repository: PlacesRepository,
) : ViewModel() {

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

    fun selectPoint(id: String?) {
        _state.update { it.copy(selectedPointId = id) }
    }
}

A animação da câmera é um efeito de UI e pode permanecer na camada Compose:

val selectedPoint = uiState.points.firstOrNull {
    it.id == uiState.selectedPointId
}

LaunchedEffect(selectedPoint?.id) {
    selectedPoint ?: return@LaunchedEffect

    cameraPositionState.animate(
        CameraUpdateFactory.newLatLngZoom(
            LatLng(selectedPoint.latitude, selectedPoint.longitude),
            15f,
        ),
    )
}

Use o ID como chave do LaunchedEffect. Se você usar o estado inteiro ou uma lista recriada como chave, a animação pode reiniciar em recomposições e “brigar” com os gestos do usuário.

Também diferencie movimento programático de movimento manual quando a busca depende da área visível. Sem isso, o fluxo pode virar um ciclo:

  1. resultado selecionado anima a câmera;
  2. câmera parada dispara nova busca;
  3. nova lista altera seleção;
  4. câmera anima novamente.

Uma solução é pesquisar apenas após ação explícita — “Buscar nesta área” — ou aplicar debounce e comparar os limites geográficos antes de consultar o backend.

Enquadre vários pontos com limites geográficos

Quando a API retorna diversos locais, um zoom fixo não serve para todas as distribuições. Calcule um LatLngBounds:

suspend fun fitPoints(
    cameraPositionState: CameraPositionState,
    points: List<MapPoint>,
    paddingPx: Int,
) {
    if (points.isEmpty()) return

    if (points.size == 1) {
        val point = points.first()
        cameraPositionState.animate(
            CameraUpdateFactory.newLatLngZoom(
                LatLng(point.latitude, point.longitude),
                15f,
            )
        )
        return
    }

    val bounds = LatLngBounds.builder().apply {
        points.forEach { point ->
            include(LatLng(point.latitude, point.longitude))
        }
    }.build()

    cameraPositionState.animate(
        CameraUpdateFactory.newLatLngBounds(bounds, paddingPx)
    )
}

Execute a atualização quando o mapa já tiver dimensões válidas. Se o cálculo ocorrer cedo demais, a operação de bounds pode falhar porque o SDK ainda não conhece o tamanho disponível. Na UI, dispare o efeito depois do layout ou utilize a estratégia recomendada pela versão atual da biblioteca.

Considere também o espaço ocupado por bottom sheets e barras. Enquadrar pontos no retângulo total e depois cobrir metade deles com uma lista deslizante produz um resultado tecnicamente correto, mas ruim para o usuário.

Solicite localização apenas no momento necessário

Se a tela oferece “perto de mim” ou acompanhamento do usuário, peça a permissão quando a pessoa acionar o recurso. O guia de permissões no Android com Kotlin aprofunda rationale, negativa e diferenças entre versões.

No manifesto, localização aproximada pode ser suficiente para descoberta regional:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Declare FINE somente quando a precisão fizer diferença real. O usuário pode conceder apenas localização aproximada, e o recurso precisa degradar com honestidade.

Em Compose, modele a decisão como estado:

@Composable
fun LocationPermissionAction(
    hasLocationPermission: Boolean,
    onPermissionChanged: (Boolean) -> Unit,
    onLocate: () -> Unit,
) {
    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { result ->
        val granted = result[Manifest.permission.ACCESS_FINE_LOCATION] == true ||
            result[Manifest.permission.ACCESS_COARSE_LOCATION] == true
        onPermissionChanged(granted)
        if (granted) onLocate()
    }

    Button(
        onClick = {
            if (hasLocationPermission) {
                onLocate()
            } else {
                launcher.launch(
                    arrayOf(
                        Manifest.permission.ACCESS_COARSE_LOCATION,
                        Manifest.permission.ACCESS_FINE_LOCATION,
                    )
                )
            }
        }
    ) {
        Text("Usar minha localização")
    }
}

Não repita automaticamente o diálogo após uma negativa. Explique o benefício, ofereça busca manual e, quando apropriado, mostre uma ação para abrir as configurações.

Obtenha uma posição do aparelho com segurança

A Fused Location Provider API pode fornecer uma posição recente ou solicitar uma leitura atual. Elas não significam a mesma coisa:

  • última localização: rápida e econômica, mas pode estar ausente ou antiga;
  • localização atual: tenta produzir uma leitura adequada dentro de critérios e tempo limitados;
  • atualizações contínuas: indicadas somente para acompanhamento que permanece ativo.

Encapsule o SDK atrás de uma interface:

data class DeviceLocation(
    val latitude: Double,
    val longitude: Double,
    val accuracyMeters: Float?,
    val capturedAtMillis: Long,
)

interface DeviceLocationSource {
    suspend fun currentLocation(): Result<DeviceLocation>
}

Uma implementação conceitual pode usar getCurrentLocation e converter a Task para coroutine com o adaptador apropriado:

class FusedDeviceLocationSource(
    private val client: FusedLocationProviderClient,
    private val permissionChecker: LocationPermissionChecker,
) : DeviceLocationSource {

    override suspend fun currentLocation(): Result<DeviceLocation> = runCatching {
        check(permissionChecker.hasAnyLocationPermission()) {
            "Permissão de localização não concedida"
        }

        val cancellationTokenSource = CancellationTokenSource()

        val location = client.getCurrentLocation(
            Priority.PRIORITY_BALANCED_POWER_ACCURACY,
            cancellationTokenSource.token,
        ).await() ?: error("Localização indisponível")

        DeviceLocation(
            latitude = location.latitude,
            longitude = location.longitude,
            accuracyMeters = location.accuracy,
            capturedAtMillis = location.time,
        )
    }
}

A assinatura e as extensões de coroutine dependem das bibliotecas adotadas. Verifique a documentação oficial de localização no Android antes de copiar o exemplo.

A tela precisa tratar pelo menos:

  • permissão ausente;
  • localização desativada no aparelho;
  • posição nula;
  • timeout;
  • coordenada com baixa precisão;
  • dispositivo sem os serviços esperados;
  • cancelamento quando a tela deixa de precisar do resultado.

Para uma busca de lojas, precisão balanceada costuma ser melhor que GPS máximo. Para navegação curva a curva, o problema é muito mais amplo que centralizar um mapa e requer desenho específico de consumo, foreground service e experiência em segundo plano.

Muitos marcadores: agrupamento e janela visível

Centenas de marcadores individuais aumentam poluição visual e podem custar desempenho. Antes de otimizar código, reduza o conjunto exibido:

  1. consulte apenas a área visível;
  2. limite resultados por zoom;
  3. agregue itens no backend quando fizer sentido;
  4. use clustering para pontos próximos;
  5. carregue detalhes somente após seleção.

As bibliotecas de utilitários e integrações opcionais do ecossistema Google Maps oferecem recursos de clustering. Confirme o artefato compatível com sua versão do Maps Compose. O importante é não transformar cada recomposição em reconstrução completa de milhares de objetos.

Mantenha listas imutáveis e IDs estáveis. Faça cálculos pesados — ordenação geográfica, conversão de modelos, criação de grupos — fora do corpo do Composable, preferencialmente no ViewModel ou em uma camada própria.

Evite também ícones de marcador gerados a partir de layouts complexos para cada frame. Cacheie recursos derivados quando a aparência não muda e teste em aparelhos intermediários, não apenas no dispositivo mais rápido da equipe.

Rotas, Places e geocodificação são serviços separados

Mostrar um mapa não cria automaticamente rotas nem transforma texto em coordenadas. Trate cada necessidade como uma capacidade separada:

  • Places: busca e detalhes de locais;
  • Routes: cálculo de trajetos e informações relacionadas;
  • geocodificação: conversão entre endereços e coordenadas;
  • Maps SDK: mapa interativo no Android.

Cada serviço pode ter configuração, cota, preço, termos e restrições próprios. Não habilite tudo “para garantir”. Se uma operação exige credencial de servidor ou lógica sensível, faça-a no backend e aplique proteção adequada; não mova uma chave privilegiada para o APK.

Ao receber latitude e longitude da sua API, valide intervalos antes de criar LatLng:

fun MapPoint.hasValidCoordinates(): Boolean {
    return latitude in -90.0..90.0 &&
        longitude in -180.0..180.0 &&
        latitude.isFinite() &&
        longitude.isFinite()
}

Dados inválidos não devem derrubar a tela inteira. Remova o ponto problemático, registre telemetria sem dados pessoais desnecessários e permita que o restante do mapa continue disponível.

Privacidade e LGPD

Localização pode revelar casa, trabalho, rotina, religião, saúde e relações pessoais. Mesmo quando o app não pretende inferir essas informações, um histórico detalhado pode permitir inferências. Aplique minimização desde o desenho:

  • peça localização apenas para uma ação clara;
  • prefira precisão aproximada quando suficiente;
  • não envie coordenadas ao backend se o cálculo pode ocorrer no aparelho;
  • não registre latitude e longitude completas em analytics;
  • defina retenção e exclusão para dados armazenados;
  • explique compartilhamento com terceiros;
  • limite acesso interno e proteja dados em trânsito e repouso;
  • não mantenha rastreamento contínuo depois que a tarefa terminou.

Eventos de analytics podem registrar “busca nesta área usada” ou “marcador selecionado”, sem anexar coordenadas exatas. Se uma métrica precisa de região, avalie agregação suficientemente ampla e documente a finalidade.

O guia de LGPD para apps Android oferece uma visão mais ampla de consentimento, minimização e ciclo de vida dos dados. A implementação técnica não substitui avaliação jurídica aplicável ao produto.

Estado, rotação e restauração do processo

O mapa possui estado interno, mas seu produto também tem filtros, item selecionado e viewport relevante. Decida explicitamente o que deve sobreviver:

  • ID do item selecionado: ViewModel e, se necessário, SavedStateHandle;
  • filtros da busca: ViewModel;
  • lista de resultados: recarregada pelo repositório ou cacheada conforme a arquitetura;
  • posição da câmera: estado do mapa ou representação serializável quando a continuidade for importante;
  • diálogo e mensagens temporárias: estado de UI consumível.

Não salve objetos do SDK ou listas enormes em SavedStateHandle. Para uma câmera, latitude, longitude, zoom e orientação já formam um estado pequeno:

data class SavedMapViewport(
    val latitude: Double,
    val longitude: Double,
    val zoom: Float,
    val bearing: Float,
    val tilt: Float,
)

Restaurar o viewport não significa sempre mover a câmera. Se a tela foi aberta por um App Link ou deep link, o destino explícito deve ter prioridade sobre a posição antiga.

Teste sem depender apenas do mapa real

Testes unitários não precisam renderizar tiles. Extraia regras puras e valide-as separadamente:

class SelectVisiblePointsTest {
    @Test
    fun `remove pontos com coordenadas invalidas`() {
        val points = listOf(
            MapPoint("ok", "Loja", null, -23.5, -46.6),
            MapPoint("bad", "Inválida", null, 120.0, -46.6),
        )

        val valid = points.filter(MapPoint::hasValidCoordinates)

        assertEquals(listOf("ok"), valid.map { it.id })
    }
}

Também teste:

  • seleção de marcador por ID;
  • regra de filtros;
  • erro e retry do repositório;
  • permissão negada;
  • localização nula ou antiga;
  • comportamento com lista vazia;
  • escolha de ponto único versus bounds;
  • cancelamento da leitura de localização.

Nos testes de UI, substitua a tela do mapa por uma abstração ou semântica observável quando o objetivo for testar fluxo de produto. Reserve testes instrumentados com o SDK real para um conjunto menor: inicialização, credencial de release, gestos principais, marcador, info window e camadas sobrepostas.

Antes de publicar, execute uma matriz manual:

CenárioResultado esperado
API key inválidaestado diagnosticável; app não fecha
sem internetUI explica indisponibilidade ou mantém dados já carregados
permissão negadamapa funciona sem posição do usuário
localização desligadabusca manual continua disponível
apenas localização aproximadarecurso aceita menor precisão ou explica limitação
zero resultadosmapa e estado vazio não entram em loop
centenas de pontosclustering ou redução mantém interação fluida
rotação/processo recriadofiltros e seleção importante são restaurados
build da lojamapa autentica com o certificado correto

Erros comuns

Versionar a API key sem restrição

Esconder o valor em um arquivo Kotlin não resolve. Retire-o do histórico quando necessário, restrinja a credencial por aplicativo e API e monitore uso anormal.

Pedir localização assim que a tela abre

O mapa pode funcionar sem a posição do usuário. Peça acesso depois que a pessoa escolher “perto de mim” ou outra função que explique o benefício.

Usar a última posição como se fosse atual

Ela pode ter sido capturada horas atrás. Verifique idade e precisão ou solicite uma leitura atual com timeout compatível com a tarefa.

Animar a câmera em toda recomposição

Coloque animações em efeitos com chaves estáveis. Caso contrário, o mapa volta ao ponto selecionado enquanto o usuário tenta arrastá-lo.

Fazer uma chamada de rede para cada movimento

A câmera emite muitas mudanças. Use gesto concluído, botão “Buscar nesta área”, debounce e comparação do viewport para controlar consultas.

Renderizar milhares de pins

Filtre pela área visível e pelo zoom, agrupe pontos e carregue detalhes sob demanda.

Guardar localização exata em analytics

Métricas de produto raramente precisam de coordenadas completas. Colete eventos sem dados pessoais desnecessários e aplique a política de retenção.

Validar somente no debug

A chave pode estar associada apenas ao certificado de debug. Teste o artefato assinado e a configuração do Play App Signing antes do lançamento.

Checklist de produção

Antes de liberar uma tela com Google Maps e Kotlin:

  • Maps SDK e somente APIs necessárias estão habilitados;
  • API key está restrita ao pacote, certificados e APIs corretos;
  • credenciais privilegiadas permanecem no backend;
  • build de release foi testada em dispositivo real;
  • mapa funciona sem localização quando possível;
  • permissão é solicitada em contexto e aceita modo aproximado;
  • posição nula, antiga e imprecisa tem tratamento;
  • câmera não reinicia durante recomposições;
  • consultas por viewport têm debounce ou ação explícita;
  • marcadores usam IDs estáveis e coordenadas validadas;
  • grande volume usa redução ou clustering;
  • filtros e seleção sobrevivem à recriação necessária;
  • logs e analytics não expõem coordenadas exatas;
  • loading, erro, vazio e modo offline têm UI definida;
  • acessibilidade dos controles sobrepostos foi revisada;
  • cotas, faturamento e alertas de consumo estão configurados.

Perguntas frequentes

Preciso pedir permissão para mostrar o Google Maps em Compose?

Não. Exibir um mapa, marcadores e áreas definidas pela aplicação não exige acesso à localização do aparelho. A permissão é necessária quando o recurso consulta ou mostra a posição do usuário.

Posso colocar a API key do Google Maps no APK?

Uma chave de cliente Android será distribuída com o aplicativo e não pode ser tratada como segredo absoluto. Restrinja-a ao pacote, aos certificados de assinatura e às APIs necessárias. Chaves privilegiadas de servidor não devem ir para o APK.

isMyLocationEnabled fornece as coordenadas do usuário?

Não. Essa propriedade controla a camada visual de localização do mapa. Para usar coordenadas em uma regra do app, consulte uma API de localização com permissão, tratamento de erro e critérios de precisão.

Como evitar que o mapa volte sozinho ao marcador?

Não execute animate diretamente no corpo do Composable. Use LaunchedEffect com uma chave estável, como o ID selecionado, e não atualize a seleção em resposta a todo movimento da câmera.

Marker clustering é obrigatório?

Não para poucos pontos. Ele se torna útil quando marcadores próximos se sobrepõem, prejudicam a leitura ou tornam a interação pesada. Antes disso, limite os dados à janela visível e ao nível de zoom.

Maps Compose substitui o ViewModel?

Não. A biblioteca oferece UI e estado específico do mapa. Resultados, filtros, seleção, erros e regras de domínio continuam pertencendo a uma arquitetura de aplicação testável.

Próximos passos

Comece pela menor entrega útil: mapa sem permissão, um conjunto pequeno de pontos vindos do ViewModel e seleção por ID. Depois valide a chave na build de release, acrescente enquadramento e somente então implemente “perto de mim” se houver uma necessidade real.

Uma boa tela de mapa não é a que reúne todas as APIs geográficas. É a que mantém a tarefa clara quando a rede falha, a localização está desativada ou o usuário prefere não conceder acesso. Com credenciais restritas, estado previsível, consultas controladas e privacidade por padrão, o Google Maps no Jetpack Compose deixa de ser uma demonstração visual e vira uma parte confiável do app Android.