Gerente de produtos no Google
O Android está se tornando um sistema de inteligência em que agentes como o Gemini orquestram tarefas em vários apps. Para desbloquear novas experiências do usuário e participar desses fluxos de trabalho de agentes, seu app precisa expor os recursos principais.
Esta sessão é um guia prático para integrar seu app à camada de inteligência do Android. Vamos analisar os principais caminhos de integração, incluindo o MCP do Android no dispositivo, o MCP do lado do servidor e a automação de tarefas, e fornecer um framework de decisão para escolher o caminho certo para seu caso de uso. Também vamos falar sobre as experiências proativas com o Contexto Pessoal.
Você aprenderá o seguinte:
AppFunctionDeclaration, forneça descrições do KDoc e evite depender de valores padrão do Kotlin.Transcrição
Certo.
Olá!
Sou Dan Galpin, da equipe de relações com desenvolvedores.
E eu sou Sarah Shamsie, gerente de produtos que trabalha com os frameworks de inteligência para desenvolvedores do Android.
Estamos muito felizes em receber você na sessão de hoje.
Estamos testemunhando uma mudança de paradigma na forma como os usuários interagem com os dispositivos.
Na era da Web, os usuários geralmente começavam as jornadas abrindo o navegador da Web e usando a função de pesquisa.
E, na era dos dispositivos móveis, o comportamento do usuário mudou para o uso de apps nativos mais especializados para realizar ações pontuais.
Veja um exemplo.
Recebo uma mensagem de um amigo me convidando para jantar.
Para responder, preciso verificar minha agenda para ver se estou livre, adicionar o evento à minha agenda, reservar um transporte se precisar de uma forma de chegar lá e, em seguida, enviar uma mensagem para meu amigo confirmando minha presença.
Antes, isso exigia o que chamamos de "app-hopping", em que eu basicamente tinha que alternar entre diferentes apps no meu dispositivo para concluir as ações e finalizar minha jornada.
Mas agora estamos na Era da Inteligência.
Na Era da Inteligência, os usuários começam as jornadas expressando intenções e metas de alto nível, em vez de ações específicas que precisam ser realizadas.
Há uma mudança fundamental em que eles veem o dispositivo como algo que realmente antecipa as necessidades e ajuda a atingir as metas, em vez de algo que apenas permite ações.
E percebemos que eles esperam que essa estrutura de inteligência funcione não apenas em dispositivos móveis, mas em um ecossistema de formatos variados, incluindo relógios, carros, TVs, laptops, XR e óculos.
Na Era da Inteligência, um único comando entre apps pode facilitar o exemplo do convite para jantar mencionado anteriormente.
Com apenas um comando, um agente no meu dispositivo pode ajudar a ativar todas as ações que preciso, desde verificar minha agenda, adicionar o evento, reservar a corrida e enviar uma mensagem, tudo de uma vez.
Hoje, vamos compartilhar mais sobre os planos do Android nesse espaço e como vocês, desenvolvedores, podem criar experiências de primeira classe.
Dan.
Obrigada, Sarah.
O Android continua adicionando inteligência ao seu núcleo, e isso oferece novas maneiras para seus apps se prepararem para essa Era da Inteligência.
O primeiro desses dois pilares de inteligência para desenvolvedores é a inteligência no app, que, obviamente, torna seus apps mais inteligentes, trazendo inteligência para eles. Você pode usar modelos no dispositivo, como o Gemini Nano 4, pelo Kit de ML para resumir conversas, reescrever respostas, entender imagens ou usar linguagem natural para chamar as funções do seu app.
Você pode até usar modelos do Gemini na nuvem diretamente no seu app com o Firebase AI Logic.
Mas nosso foco hoje é o segundo pilar: garantir que os recursos do seu app estejam disponíveis no sistema de inteligência.
E o que é um sistema de inteligência?
Além de falar e hospedar as experiências de comando que a Sarah mencionou, é um sistema que permite integrações profundas para melhorar as experiências em todo o ecossistema de apps.
Por exemplo, os dispositivos projetados para a Gemini Intelligence usam esses pontos de integração para oferecer sugestões, notificações resumidas, entrada de voz aprimorada e preenchimento automático de formulários.
Agora, as sugestões fazem parte de um recurso chamado assistência proativa.
Até agora, falamos sobre assistência por comando.
Todos nós estamos acostumados com isso, em que um usuário diz a um agente para realizar uma ou mais tarefas em nome dele.
Mas a assistência proativa é, na verdade, sobre antecipar a próxima ação ou ações que um usuário pode realizar.
Voltando ao exemplo do jantar, o agente pode sugerir proativamente ao usuário, em resposta à mensagem, que adicione o convite à agenda ou reserve um transporte por aplicativo.
Mas como ele realmente solicita o usuário?
A assistência proativa pode mostrar ações em lugares como a tela de bloqueio, notificações, teclados virtuais e até mesmo em apps selecionados.
Então, como fazer seu app aparecer nesses chips proativos para direcionar o tráfego para você?
Bem, você já está fazendo coisas como criar com a versão mais recente do SDK e com nossas ferramentas mais modernas, mas o melhor que você pode fazer agora é garantir que seu app também esteja pronto para a assistência por comando, que é o assunto que a Sarah vai abordar a seguir.
Obrigado, Dan.
Certo, então a assistência solicitada envolve os usuários acessando diretamente um agente e pedindo que ele inicie e organize tarefas, até mesmo em vários apps.
Isso pode automatizar fluxos de trabalho que antes eram manuais e demorados.
Para nosso sistema de inteligência no Android, estamos focados principalmente em agentes do sistema, como o Gemini, que podem acessar apps em todo o dispositivo do usuário.
Há algumas maneiras diferentes de seu app aparecer nessa experiência e interagir com o agente do sistema.
Temos nossa solução de automação de tarefas baseada em plataforma e as integrações diretas de API no agente do sistema.
Sempre recomendamos criar com integrações de API diretas sempre que possível.
Eles são controlados pelo desenvolvedor, então você pode decidir e controlar como o app e a marca aparecem.
Eles também são mais rápidos e confiáveis, oferecendo uma experiência mais premium aos usuários.
Há dois caminhos de API que você pode considerar ao fazer a integração com o agente.
O Protocolo de Contexto de Modelo (MCP, na sigla em inglês) é a prática recomendada do setor para chamadas de função do lado do servidor.
Mas para chamadas de ferramentas no dispositivo, estamos anunciando formalmente o MCP do Android, o framework unificado de chamada de função no Android.
O MCP do Android usa nossa biblioteca AppFunctions do Jetpack, que se alinha de perto, tanto técnica quanto conceitualmente, ao MCP, além de oferecer suporte a uma implementação do Kotlin familiar e idiomática que muitos desenvolvedores Android conhecem.
Isso torna a implementação da API relativamente intuitiva e fácil de usar, e você pode aproveitar o que aprendeu com uma integração do MCP do lado do servidor já estabelecida.
Muitas primitivas do MCP são mapeadas de forma direta para uma construção de AppFunctions do Jetpack.
Vamos conferir alguns exemplos para ilustrar isso.
Um endpoint de ferramenta do MCP é mapeado para uma função do Kotlin anotada com AppFunctionDeclaration.
Seus modelos pydantic são mapeados para classes de dados Kotlin anotadas com AppFunctionSerializable.
No entanto, em vez de usar HTTP transmissível ou transporte de E/S padrão, o MCP do Android é executado no IPC otimizado do Android Binder.
Hoje, temos o prazer de anunciar que a biblioteca do Jetpack, AppFunctions, já está na versão Beta.
Ok, então você deve estar se perguntando quando faz sentido usar um caminho de API e quando faz sentido usar o MCP do Android, especificamente.
Como princípio geral, o MCP do Android é especialmente útil quando você quer usar o contexto local ou no dispositivo para atender a uma jornada do usuário.
Vamos conferir alguns exemplos em que o MCP do Android realmente se destaca.
Especificamente, considere o MCP do Android quando precisar acessar dados locais criptografados.
Isso é especialmente importante para apps de mensagens e comunicações, em que o histórico de mensagens precisa permanecer no dispositivo devido à criptografia de ponta a ponta.
O MCP do Android também é útil para ações puramente no dispositivo e off-line, quando os agentes oferecem suporte a isso.
Nosso parceiro do Wear OS, o Samsung Health, lançou uma função para iniciar uma corrida e exercícios, que são orquestrados inteiramente no dispositivo.
Os usuários geralmente querem que esses tipos de ações sejam feitos de forma muito rápida, com alta confiabilidade, baixa latência e, às vezes, até mesmo em ambientes de baixa conectividade. Garantir que toda a chamada de ferramenta e o roteamento de consultas possam acontecer no dispositivo garante isso.
O MCP do Android também é o caminho de API preferencial se você quiser acessar sensores do dispositivo ou interagir diretamente com o hardware dele.
Por exemplo, muitos apps automotivos querem permitir que os usuários destranquem as portas do carro instantaneamente usando a conectividade UWB, muitas vezes sem usar as mãos.
O MCP do Android pode ajudar com isso.
Por fim, é a solução ideal se não houver um Cloud Endpoints disponível para seus apps ou APIs.
Muitos dos apps próprios dos fabricantes de dispositivos são apenas do lado do cliente e, por isso, usam o MCP do Android.
Por exemplo, o Gemini pode ser usado para pesquisar e recuperar fotos de apps de galeria que são apenas do lado do cliente, como o Xiaomi Gallery.
Aqui, posso abrir o Gemini e pedir: "Encontre as fotos do robô do Android que tirei na semana passada na Xiaomi Gallery".
A Xiaomi expôs uma função do app para pesquisar fotos, que o Gemini chama. As fotos relevantes são retornadas e mostradas diretamente na interface do app do Gemini, para que o usuário não precise sair.
E estamos usando a mesma arquitetura para ações no dispositivo no SO Android.
O MCP do Android é a base do recurso Ajuda do Dispositivo, lançado recentemente, que é uma integração direta do SO Android ao app Gemini.
Esse recurso já foi lançado em dispositivos Pixel e Lenovo e está sendo escalonado horizontalmente para o ecossistema mais amplo de OEMs do Android.
Os usuários podem usar a Ajuda do Dispositivo acessando o Gemini e perguntando como otimizar as configurações do dispositivo e do sistema para melhorar a experiência.
Por exemplo, em vez de procurar em vários menus, um usuário daltônico pode perguntar ao Gemini: "Sou daltônico.
Como posso fazer este celular funcionar melhor para mim?"
O Gemini vai chamar e invocar funções do app local, analisar o pedido de forma inteligente e sugerir maneiras reais de o usuário mudar as configurações do sistema e as configurações do dispositivo para melhorar a experiência.
Em seguida, os controles no app aparecem diretamente na interface do app do Gemini, para que os usuários possam assumir o controle imediato.
Por exemplo, para atender a essa solicitação de otimização para daltônicos, o Gemini mostrou esse botão de correção de cor, para que os usuários possam fazer a mudança rapidamente.
As experiências de MCP do Android já estão disponíveis em alguns dispositivos, e estamos trabalhando com vários parceiros para trazer ainda mais casos de uso para o ecossistema Android.
Embora as declarações explícitas de ferramentas do MCP do Android sejam nosso padrão de excelência, sabemos que haverá casos em que os usuários pedirão aos agentes para concluir casos de uso complexos ou menos comuns. É aí que entra nosso recurso de automação de tarefas orientado pela plataforma.
Nesse modelo, o agente pré-carregado do OEM pode usar as APIs de controle de computador do Android para simular eventos de toque em uma superfície de app.
Isso não exige mudanças no código por parte dos desenvolvedores, e o agente precisa tocar na interface como um usuário faria, analisando a tela do app.
Para garantir o controle e a segurança do usuário, é fundamental que os agentes façam uma pausa em ações potencialmente sensíveis, como fluxos de pagamento, e solicitem a confirmação do usuário para continuar.
Vamos conferir um exemplo na prática.
Aqui, um usuário tirou uma foto de um passeio de café e chocolate e quer pedir ao Gemini para procurar na Expedia um passeio semelhante, mas para seis pessoas.
Sem precisar de uma integração direta com a API, o Gemini navega pela interface da Expedia em segundo plano da mesma forma que um usuário faria.
Em seguida, ele mostra as opções relevantes que encontrou e as entrega ao usuário quando chega a hora da confirmação final e da reserva.
É importante perceber que esses três caminhos diferentes não são mutuamente exclusivos e, na verdade, são melhores juntos.
Usuários e desenvolvedores devem esperar que os agentes consigam coordenar ferramentas em implementações de servidor e também no app Android local para oferecer a experiência mais abrangente e holística aos usuários.
Certo.
Obrigada, Sarah.
Vamos nos aprofundar um pouco mais no MCP do Android.
E, quando possível, quero que você saiba que estou extraindo os snippets de código do MCP do Jetpacker, que é nosso app de exemplo de viagem, criado com a ajuda do Antigravity.
Você já viu isso na palestra.
Ele tem vários recursos inteligentes, incluindo integrações no dispositivo com as APIs de IA generativa e de reconhecimento de fala do Kit de ML.
Agora, o MCP do Android e o MCP baseado na nuvem têm uma estrutura semelhante.
Ambos têm clientes e servidores.
No mundo do MCP, os servidores fornecem ferramentas, e os clientes podem descobrir e chamar essas ferramentas.
Agora, com o MCP do Android, seu app é o servidor, e o agente é o cliente.
A plataforma Android desempenha o papel de registro de ferramentas.
Agora, os apps começam registrando um serviço para fornecer o servidor, e vamos começar adicionando as dependências da biblioteca AppFunctions do Jetpack ao nosso exemplo aqui.
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)Em seguida, vamos usar as anotações do KSP para ajudar a criar um AppFunctionService.
Agora, esse serviço significa que as funções do app podem respeitar adequadamente os ciclos de vida do Android.
Podemos usar a injeção de dependência com o Hilt para injetar repositórios de dados que as AppFunctions podem usar.
@RequiresApi(36)
@AndroidEntryPoint
@AppFunctionServiceEntryPoint(
serviceName = "JetPackerAppFunctionService",
appFunctionXmlFileName = "jetpacker_app_function_service"
)
abstract class BaseJetPackerAppFunctionService : AppFunctionService() {
@Inject internal lateinit var tripDao: TripDao
@Inject internal lateinit var expenseDao: ExpenseDao
...Entendi.
Vamos voltar ao nosso diagrama.
Depois de configurar o serviço para nosso servidor MCP do Android, o app registra ferramentas na forma de AppFunctions, além de todos os modelos de dados associados usados por elas.
Agora você pode registrar um modelo de dados usando a anotação AppFunctionSerializable, e este KDoc aqui é bem legal.
/** Represents an event in a trip itinerary. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class TimelineEventSerializable(
/** The event's unique identifier. */
val id: String,
/** The trip ID this event belongs to. */
val tripId: String,
/** The type of event (e.g., TRANSPORTATION, ACCOMMODATION, FOOD_AND_DRINK). */
val type: String,
/** The timestamp of the event. */
val timestamp: Long,
/** The title of the event. */
val title: String,
/** The location of the event. */
val location: String,
/** A description of the event. */
val description: String?,
/** The title of the trip this event belongs to. */
val tripTitle: String? = null,
/** Optional title of a matched itinerary event when added as a reminder. */
val matchedEventTitle: String? = null,
/** Execution status of the tool call (e.g., SUCCESS). */
val status: String? = "SUCCESS",
/** Human-readable completion message instructing the agent not to repeat the call. */
val message: String? = null,
)Ele vai se tornar metadados usados pelo agente para entender como usar o modelo.
Use a anotação AppFunctionDeclaration para registrar sua função como uma ferramenta.
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
val trip = resolveTrip(tripId = tripId)
val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
val events =
if (dbEvents.isNotEmpty()) { dbEvents } else {
val fallback = DummyData.events.filter { it.tripId == targetTripId }
if (fallback.isNotEmpty()) { eventDao.insertEvents(fallback) }
fallback
}
events.map { it.toSerializable(tripTitle = trip?.title) }
}
}E com isDescribedByKDoc definido como verdadeiro, o KDoc fornece ao agente uma descrição para entender o que sua função faz, além de ajudar os humanos a entendê-la também.
/**
* Retrieves the itinerary events (with their eventId, title, type, and location) for a specific
* trip. Note: getItinerary returns event summaries only; call getEventDetails(eventId) with a
* specific event's ID to retrieve ticket prices, hotel nightly rates, baggage allowance, or
* visitor tips.
*
* @param tripId Optional ID of the trip (e.g., '2026-1'). Defaults to the active trip if omitted.
* @return A list of events in the itinerary.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
val trip = resolveTrip(tripId = tripId)
val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
...Confira algumas práticas recomendadas para o design de parâmetros.
/**
* @param tripId Optional ID of the trip (e.g., '2026-1').
* Defaults to the active trip if omitted.
* @return A list of events in the itinerary.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
// resolveTrip defaults to the active trip if tripId == null
val trip = resolveTrip(tripId = tripId)Primeiro, concentre-se no subconjunto mínimo de parâmetros com nomes claros e distintos.
Torne os parâmetros não essenciais anuláveis e resolva os padrões deles no corpo da função.
Essa não é apenas uma prática recomendada.
Isso não vai funcionar, porque os valores padrão do Kotlin serão ignorados, já que a biblioteca AppFunctions transmite nulo para parâmetros anuláveis omitidos.
Não vou nem mostrar um exemplo do primeiro, mas não use parâmetros abrangentes.
Coisas como contexto completo ou histórico de conversas são ruins.
Eles podem fazer com que os agentes transmitam grandes quantidades de dados e levem à exfiltração, o que é uma prática ruim.
Como a AppFunction é executada por padrão na linha de execução de interface, é importante garantir que qualquer operação de longa duração aconteça em segundo plano, escolhendo o agente de corrotina adequado.
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
val trip = resolveTrip(tripId = tripId)
val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?:
tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
val events =
if (dbEvents.isNotEmpty()) { dbEvents } else {
val fallback = DummyData.events.filter { it.tripId == targetTripId }
...É recomendável gerar exceções de AppFunction quando necessário com informações detalhadas para que o agente possa entender como resolver um erro.
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
message: Message
): Result {
...
throw AppFunctionException(
errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
errorMessage = "To address has invalid syntax!"
)
}Certo, agora que temos uma AppFunction, vamos voltar ao diagrama de arquitetura.
O app cliente do agente se registra na plataforma para chamar AppFunctions.
Por exemplo, o Gemini.
Para isso, eles precisam ter uma permissão privilegiada, e os agentes registrados podem procurar metadados de AppFunction na plataforma Android e decidir se devem executar uma ou mais AppFunctions registradas para atender à solicitação de um usuário.
Falando em uma ou mais AppFunctions, como o agente decide?
Por fim, você precisa descrever os recursos do seu app em um arquivo de metadados.
<AppFunctionAppMetadata
xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
appfn:description="This app manages trips and travel expenses.
Operational Patterns:
- Use 'getAllTrips' to find the correct 'tripId' before
adding expenses or managing trip details.
Constraints:
- Expenses must be associated with a valid trip ID."
appfn:displayDescription="@string/user_visible_description" />Os agentes selecionam as ferramentas lendo primeiro essa descrição no AppFunctionAppMetadata e, em seguida, lendo as descrições com as AppFunctions individuais.
Por isso, é importante focar o início da descrição em uma breve explicação do domínio principal do app ou das ferramentas.
Em seguida, você pode usar padrões operacionais para ajudar o agente a entender o fluxo lógico das operações. Isso é bom para definir pré-requisitos ou direcionar o agente para os melhores caminhos para concluir a tarefa.
E você pode adicionar uma seção sobre restrições que define limites lógicos de tipos de dados de entrada ou reforça padrões operacionais.
Esses são os conceitos básicos do MCP do Android, mas há mais coisas que você pode fazer.
Uma coisa legal que você pode fazer é ativar e desativar as AppFunctions de forma dinâmica.
viewModelScope.launch {
val newState =
if (enabled) {
AppFunctionManager.APP_FUNCTION_STATE_ENABLED
} else {
AppFunctionManager.APP_FUNCTION_STATE_DISABLED
}
appFunctionManager?.setAppFunctionEnabled(id, newState)
}Isso é importante, por exemplo, se você quiser que a AppFunction só esteja disponível quando o usuário estiver conectado.
Outra coisa que você pode fazer é criar AppFunctions com escopo para uma tela ou atividade específica no seu app.
@AppFunctionSignature(
scope = AppFunctionMetadata.SCOPE_ACTIVITY,
appFunctionXmlFileName = "cart_functions"
)
fun interface AddCurrentItemToCart {
/** Adds the item currently shown on screen to the cart. Returns the new cart size. */
suspend fun addToCart(quantity: Int): Int
}Portanto, em vez de declarar a AppFunction, declare AppFunctionSignature.
Em seguida, você pode implementar um gerenciador para a AppFunction na sua interface.
val addToCartAppFunctionAdapter =
appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
coroutineScope.launch {
appFunctionManager.handleAppFunction(
addToCartAppFunctionAdapter.adapt { quantity ->
val newCartSize = cart.addItem(quantity)
newCartSize
}
)
}E o legal é que o ciclo de vida dos gerenciadores é vinculado automaticamente ao ciclo de vida do escopo de corrotina que você usa.
Agora, você vai querer armazenar em cache esse adaptador de solicitação de AppFunctions, ou melhor, o adaptador de solicitação de AppFunctions de gerenciamento, já que a biblioteca usa reflexão em segundo plano para instanciá-lo.
Também há uma funcionalidade para criar AppFunctions com escopo, cujo registro não está diretamente vinculado ao ciclo de vida do Android.
Por fim, temos o nível de acesso.
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
...Você pode usar o nível de acesso próprio, do sistema ou confiável, que indica quais funções são exclusivas para o agente no app, como um criado com a IA generativa do Kit de ML, quais estão disponíveis apenas para o sistema e quais estão disponíveis para agentes de sistema confiáveis.
E para ajudar você a criar seu próprio agente no app, já anunciamos que o Kit de ML oferece suporte nativo às AppFunctions como parte do framework unificado de chamada de ferramentas.
Você as define uma vez e tem cobertura tanto para agentes no nível do sistema quanto para modelos locais no app, o que evita declarações de ferramentas fragmentadas.
Para saber mais, assista à palestra "Desbloqueie recursos agênticos no seu app com a IA generativa do Kit de ML" ou confira as novidades do Jetpack.
É bem legal.
Vamos voltar para a Sarah.
Certo.
O Dan acabou de mostrar como o MCP do Android funciona por dentro.
Vamos colocar em prática.
Lembre-se de que seu app é um servidor e os agentes são o cliente. Portanto, o objetivo é garantir que seu app seja um bom servidor para eles.
Vamos acompanhar o Jetpacker, nosso app de exemplo de viagem, desde a primeira declaração de ferramenta até uma chamada de função real pelo Gemini.
Você pode escrever suas AppFunctions manualmente ou usar um agente de programação para ajudar você a começar.
Lançamos nossa habilidade de AppFunctions, que pode ajudar você a começar a escrever suas funções, que podem ser conectadas ao Android Studio pela CLI do Android ou usadas com o agente de programação de sua escolha.
android skills add appfunctionsA habilidade funciona em três etapas.
Primeiro, ele analisa sua base de código para identificar jornadas do usuário que seriam ferramentas úteis, como tocar mídia, criar uma nota ou enviar uma mensagem.
Em seguida, ele gera declarações de AppFunction que expõem essas jornadas, com a implementação e o suporte da plataforma necessários.
Por exemplo, Gradle e KSP, XML de metadados do app e injeção de dependência.
Por fim, ele vai refinar as descrições do KDoc, porque é exatamente isso que o agente usa para decidir quando e como chamar sua função.
Este é o tipo de resultado que estamos procurando.
Esta é uma função addExpense para o Jetpacker.
/**
* Adds an expense to a trip.
*
* @param tripId ID of the trip. If null, the active trip is used.
* @param category Expense category. If null, it's inferred from the title.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun addExpense(
title: String,
amount: Double,
currency: String,
tripId: String? = null,
category: String? = null,
): ExpenseSerializable = withContext(Dispatchers.IO) {
if (amount <= 0.0) {
throw AppFunctionInvalidArgumentException("amount must be greater than zero")
}
val trip = resolveTrip(tripId)
?: throw AppFunctionElementNotFoundException("No trip with ID $tripId")
val expense = Expense(trip.id, title, amount, currency,
category = category ?: inferExpenseCategory(title))
expenseDao.insertExpense(expense)
expense.toSerializable(remainingBudgetUsd(trip.id))
}A descrição é uma frase de uma linha que começa com um verbo e um recurso: adiciona uma despesa a uma viagem.
Os campos de viagem e categoria podem ser nulos porque os usuários nem sempre especificam isso, mas a função os resolve no corpo.
Quando uma solicitação não é bem-sucedida, ela gera um AppFunctionException para que o agente saiba o que deu errado.
Agora vamos falar sobre testes.
Suas funções agora são serviços que os agentes podem usar.
Portanto, há duas partes que você precisa testar: o comportamento da função e o comportamento do agente.
Para sua função, vamos começar com o teste de unidade que você provavelmente já escreveu, mas há algumas coisas que você precisa considerar quando o agente é quem realmente faz a chamada de função.
Primeiro, os valores padrão do Kotlin não se aplicam aqui.
Um parâmetro omitido chega como nulo, falso ou 0.
Portanto, um limite que antes era de 10 por padrão será 0 aqui.
A chamada também começa na linha de execução principal, e chamar sua função de um teste nem sempre deixa isso claro. Por isso, execute a E/S padrão em um agente em segundo plano.
Quando algo falhar, gere um AppFunctionException.
Qualquer outra coisa vai chegar ao agente como um erro genérico desconhecido com a mensagem de exceção bruta anexada.
As entradas vão vir de um modelo, então é importante testar cenários extremos, como espaços em branco, sinônimos de dólares, como USD, e valores fora do intervalo.
No lado do agente, você precisa saber se ele escolhe a ferramenta no momento certo e se executa a função corretamente.
Cada agente se comporta de maneira um pouco diferente. Por isso, é importante testar suas funções com qualquer agente que você espera que as consuma.
Você pode usar nosso agente de amostra de código aberto ou o Gemini para testar essa integração.
Se você quiser ver como o Gemini interage com suas AppFunctions ou iniciar um MCP do Android no app do Gemini, use a Plataforma de Apps Conectados, que lançamos hoje e está em pré-lançamento.
Para saber mais, confira nossa palestra anterior sobre como estender seu app para o Gemini e a Pesquisa usando a Connected Apps Platform.
Depois que você fizer parte do programa de pré-lançamento, o processo para testar com o Gemini terá duas etapas.
Primeiro, no Google Connected Console, vamos criar um app conectado que o Gemini possa usar.
Em seguida, no Play Console, vamos fazer upload do pacote de apps para acessar as AppFunctions e iniciar uma faixa de teste interno para que os testadores possam validar as funções.
Vamos começar no Google Connected Console.
Aqui, você vai criar um app conectado usando o botão "Criar um app" no canto superior direito.
Ao criar o app conectado, escolha "Android MCP" nesta tela.
Se você estivesse criando uma implementação do MCP no servidor, seria aqui que você faria o upload do código do servidor.
Ao clicar em "Configurar", todos os apps do Google Play associados à sua conta de desenvolvedor vão aparecer aqui, prontos para serem vinculados ao seu app conectado.
Se você ainda não tiver um app do Google Play, crie um no Play Console.
Aqui, já temos o Jetpacker, então vamos vincular essa conta.
Em seguida, adicione os detalhes que os usuários vão ver no front-end do Gemini quando tentarem interagir com seu app.
Isso inclui o ícone do app, o nome de exibição, os recursos do app conectado e exemplos de comandos que os usuários podem usar para interagir com ele.
Eles precisam ser descritivos para orientar melhor os usuários.
Pronto, isso conclui a configuração do app conectado, mas você pode estar se perguntando como o app conectado realmente acessa as funções.
Como as funções estão contidas no código do app, agora precisamos acessar o Play Console, fazer upload do pacote de app e criar uma faixa de teste para que possamos testar com usuários reais.
Nesta tela, vamos fazer upload do pacote de app e, em seguida, o Google Play vai detectar as AppFunctions, que podem ser vinculadas diretamente ao app conectado.
Agora, o app conectado do Jetpack está completo com as funções integradas.
Em seguida, precisamos adicionar testadores para que alguém esteja pronto para validar suas funções e garantir que elas estejam funcionando corretamente.
Clique nas versões do app conectado aqui.
Nessa tela, você pode selecionar "Gerenciar testadores" e configurar uma lista limitada de testadores que poderão acessar seu app conectado no Gemini quando ele estiver disponível.
É possível adicionar testadores a essa faixa de teste da mesma forma que você faria para qualquer outra versão.
Confira se você tem a mesma lista de testadores para o app conectado e para o app do Google Play.
Agora vamos testar a integração.
Este é o app Jetpacker no smartphone de um usuário, já com uma viagem registrada para este evento.
Vou abrir o Gemini para conectar o app.
Preciso garantir que a mesma versão do app que enviamos ao Google Play esteja instalada no meu dispositivo e que a conta do Gemini usada tenha sido adicionada à lista de testadores.
Aqui, vou mencionar o Jetpacker e pedir o número de confirmação do meu quarto de hotel em Mountain View.
O Gemini chama as AppFunctions do Jetpacker no dispositivo e responde: "RED11223 no Redwood Grove Hotel".
Em seguida, vou perguntar: "Quanto orçamento ainda tenho para o ADS?"
E a chamada de função vai retornar US $1.850.
Agora, quero fazer uma mudança, então vou pedir ao Gemini para adicionar US $45 para o Uber do aeroporto à minha viagem para ADS.
O Gemini chama nossa função addExpense anterior para adicionar isso diretamente ao meu itinerário.
E quando abro o Jetpacker, a despesa já está lá, e o orçamento é reduzido de acordo.
Se você quiser começar a criar apps conectados para o Gemini hoje mesmo, peça acesso à Connected Apps Platform, que está em pré-lançamento.
Certo.
Vamos concluir.
Estamos no início de uma grande mudança na computação móvel, e é hora de garantir que seu app esteja pronto.
Comece identificando as principais jornadas do usuário de alto valor do seu app.
Exponha o back-end da nuvem usando o MCP do lado do servidor e use o MCP do Android para trabalhar com dados criptografados de ponta a ponta, operações locais, sensores do dispositivo e outros recursos no dispositivo.
Para criar protótipos, faça o download do Android Studio e da CLI do Android.
Você pode adicionar a dependência Beta do AndroidX AppFunctions aos seus arquivos de build e testar sua primeira função no dispositivo localmente.
Para se registrar e começar a usar o Gemini em produção, use este QR code.
E, claro, não deixe de conferir estas palestras relacionadas sobre como trabalhar com a Connected Apps Platform e como usar as AppFunctions internas com o Kit de ML ou com seu próprio agente para impulsionar seu agente no app.
Por fim, agradecemos sua atenção.
Mal podemos esperar para ver todas as experiências incríveis que você está criando para o Android.
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)@RequiresApi(36)
@AndroidEntryPoint
@AppFunctionServiceEntryPoint(
serviceName = "JetPackerAppFunctionService",
appFunctionXmlFileName = "jetpacker_app_function_service"
)
abstract class BaseJetPackerAppFunctionService : AppFunctionService() {
@Inject internal lateinit var tripDao: TripDao
@Inject internal lateinit var expenseDao: ExpenseDao
.../** Represents an event in a trip itinerary. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class TimelineEventSerializable(
/** The event's unique identifier. */
val id: String,
/** The trip ID this event belongs to. */
val tripId: String,
/** The type of event (e.g., TRANSPORTATION, ACCOMMODATION, FOOD_AND_DRINK). */
val type: String,
/** The timestamp of the event. */
val timestamp: Long,
/** The title of the event. */
val title: String,
/** The location of the event. */
val location: String,
/** A description of the event. */
val description: String?,
/** The title of the trip this event belongs to. */
val tripTitle: String? = null,
/** Optional title of a matched itinerary event when added as a reminder. */
val matchedEventTitle: String? = null,
/** Execution status of the tool call (e.g., SUCCESS). */
val status: String? = "SUCCESS",
/** Human-readable completion message instructing the agent not to repeat the call. */
val message: String? = null,
)@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
val trip = resolveTrip(tripId = tripId)
val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
val events =
if (dbEvents.isNotEmpty()) { dbEvents } else {
val fallback = DummyData.events.filter { it.tripId == targetTripId }
if (fallback.isNotEmpty()) { eventDao.insertEvents(fallback) }
fallback
}
events.map { it.toSerializable(tripTitle = trip?.title) }
}
}/**
* Retrieves the itinerary events (with their eventId, title, type, and location) for a specific
* trip. Note: getItinerary returns event summaries only; call getEventDetails(eventId) with a
* specific event's ID to retrieve ticket prices, hotel nightly rates, baggage allowance, or
* visitor tips.
*
* @param tripId Optional ID of the trip (e.g., '2026-1'). Defaults to the active trip if omitted.
* @return A list of events in the itinerary.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
val trip = resolveTrip(tripId = tripId)
val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
.../**
* @param tripId Optional ID of the trip (e.g., '2026-1').
* Defaults to the active trip if omitted.
* @return A list of events in the itinerary.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
// resolveTrip defaults to the active trip if tripId == null
val trip = resolveTrip(tripId = tripId)@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
val trip = resolveTrip(tripId = tripId)
val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?:
tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
val events =
if (dbEvents.isNotEmpty()) { dbEvents } else {
val fallback = DummyData.events.filter { it.tripId == targetTripId }
...@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
message: Message
): Result {
...
throw AppFunctionException(
errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
errorMessage = "To address has invalid syntax!"
)
}<AppFunctionAppMetadata
xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
appfn:description="This app manages trips and travel expenses.
Operational Patterns:
- Use 'getAllTrips' to find the correct 'tripId' before
adding expenses or managing trip details.
Constraints:
- Expenses must be associated with a valid trip ID."
appfn:displayDescription="@string/user_visible_description" />viewModelScope.launch {
val newState =
if (enabled) {
AppFunctionManager.APP_FUNCTION_STATE_ENABLED
} else {
AppFunctionManager.APP_FUNCTION_STATE_DISABLED
}
appFunctionManager?.setAppFunctionEnabled(id, newState)
}@AppFunctionSignature(
scope = AppFunctionMetadata.SCOPE_ACTIVITY,
appFunctionXmlFileName = "cart_functions"
)
fun interface AddCurrentItemToCart {
/** Adds the item currently shown on screen to the cart. Returns the new cart size. */
suspend fun addToCart(quantity: Int): Int
}val addToCartAppFunctionAdapter =
appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
coroutineScope.launch {
appFunctionManager.handleAppFunction(
addToCartAppFunctionAdapter.adapt { quantity ->
val newCartSize = cart.addItem(quantity)
newCartSize
}
)
}@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
...android skills add appfunctions/**
* Adds an expense to a trip.
*
* @param tripId ID of the trip. If null, the active trip is used.
* @param category Expense category. If null, it's inferred from the title.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun addExpense(
title: String,
amount: Double,
currency: String,
tripId: String? = null,
category: String? = null,
): ExpenseSerializable = withContext(Dispatchers.IO) {
if (amount <= 0.0) {
throw AppFunctionInvalidArgumentException("amount must be greater than zero")
}
val trip = resolveTrip(tripId)
?: throw AppFunctionElementNotFoundException("No trip with ID $tripId")
val expense = Expense(trip.id, title, amount, currency,
category = category ?: inferExpenseCategory(title))
expenseDao.insertExpense(expense)
expense.toSerializable(remainingBudgetUsd(trip.id))
}