⚡ Current on the latest. AppFunctions just landed — Android 16+, rolling out on Galaxy S26 / Pixel 10. Here's how it works end-to-end, and how it slots into my own device-info app ANTAR to make it agent-ready for when the Gemini integration opens past preview. Thoughts and questions welcome in the comments.
- What AppFunctions actually is, and why "on-device MCP" is the right way to think about it
- The mental model: MCP (cloud tools) vs AppFunctions (on-device tools)
- Real, copy-pasteable code — Gradle setup, a working
@AppFunction, serializable types, the app wiring, runtime enable/disable, and how to verify it landed - Where it fits in the bigger 2026 Android agent stack (Gemini Nano, ADK for Android, Firebase AI Logic)
- The honest gaps — what's public vs private-preview today
If you've wired up the Model Context Protocol (MCP), you know the shape: you give an AI assistant a set of tools (functions it can call), and the model decides when to call them. MCP normally runs those tools on a server, over the network.
AppFunctions is the same idea, except the tools live inside your Android app and run on the phone. Google describes it directly: AppFunctions lets your app "act as an on-device Model Context Protocol (MCP) server."
Think of it like this: MCP is a USB port for a cloud AI. AppFunctions is a USB port for the AI that's already on your phone.
Take ANTAR, my device-info app. Normally the user opens it and taps to the Battery screen. With AppFunctions, the phone's AI can just ask ANTAR:
Without AppFunctions With AppFunctions
──────────────────── ──────────────────
User → opens ANTAR User → "Hey Gemini, what's my
→ taps to Battery battery health?"
→ reads the value Gemini → finds ANTAR's @AppFunction
→ calls getBatteryInfo() ON DEVICE
→ ANTAR returns level, temp, health
Your app goes from a thing the user navigates to a thing the assistant can drive.
| 🧩 AppFunctions | ☁️ Standard MCP | |
|---|---|---|
| Where tools run | On the device (OS-level hooks) | On a cloud server |
| Network | None — local call | Round-trip every call |
| Latency | Low | Network-bound |
| Data | Stays on the phone | Leaves the device |
| Platform | Android only | Platform-agnostic |
| You maintain | Annotated functions in your app | A separate service |
AppFunctions is the tools layer. It clicks together with the rest of what Google shipped around Google I/O '26:
┌─────────────────────────────────────────┐
│ ADK for Android (0.1.0) │ ← agent orchestration
│ defines agents, on-device + hybrid │
└───────────────┬───────────────────────────┘
│ runs on
┌───────────────▼───────────────────────────┐
│ Gemini Nano (via AICore / ML Kit GenAI) │ ← the on-device model
└───────────────┬───────────────────────────┘
│ calls your tools via
┌───────────────▼───────────────────────────┐
│ AppFunctions (@AppFunction in YOUR app) │ ← YOU build this
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Firebase AI Logic — PREFER_ON_DEVICE / │ ← routes to cloud
│ PREFER_CLOUD when the local model isn't enough│ when needed
└─────────────────────────────────────────────┘
The part you own is the bottom box: the functions. Everything above is plumbing Google provides.
Everything in this section is from the official AppFunctions guide and is accurate to
1.0.0-alpha09. I've kept it copy-pasteable.
compileSdk must be 36+. Add the three artifacts and the KSP aggregation flag:
// app/build.gradle.kts
ksp {
arg("appfunctions:aggregateAppFunctions", "true")
}
dependencies {
implementation("androidx.appfunctions:appfunctions:1.0.0-alpha09")
implementation("androidx.appfunctions:appfunctions-service:1.0.0-alpha09")
ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha09")
}Two annotations do the work:
@AppFunction— marks a suspend function as agent-callable. First parameter is alwaysAppFunctionContext.@AppFunctionSerializable— marks the parameter/return data classes so they can cross the boundary.
isDescribedByKDoc = true is the clever bit: your KDoc comment becomes the description the agent reads. So write the doc like you're explaining the function to the assistant.
Here it is for ANTAR — exposing the battery snapshot. The annotations and setup are exactly the official pattern; I've just pointed the function at ANTAR's existing battery repository:
package com.ashes.dev.works.system.core.internals.antar.appfunctions
import androidx.appfunctions.AppFunctionContext
import androidx.appfunctions.AppFunctionSerializable
import androidx.appfunctions.service.AppFunction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class DeviceFunctions @Inject constructor(
private val batteryRepository: BatteryRepository
) {
/** A snapshot of the device's battery. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class BatteryInfo(
/** Charge level, 0–100. */
val levelPercent: Int,
/** Battery temperature in Celsius. */
val temperatureC: Float,
/** Health, e.g. "Good", "Overheat". */
val health: String,
/** True while the device is charging. */
val isCharging: Boolean
)
/**
* Returns the current battery status of this device — charge level,
* temperature, health, and whether it's charging.
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun getBatteryInfo(
appFunctionContext: AppFunctionContext
): BatteryInfo = withContext(Dispatchers.IO) {
batteryRepository.snapshot() // ANTAR's existing battery read
}
}That's the whole provider side. The KDoc on getBatteryInfo is what tells Gemini when to call it; the BatteryInfo fields are what it gets back. (For functions that take input, the framework also gives you typed exceptions like AppFunctionInvalidArgumentException so a bad call returns a structured error, not a stack trace.)
The compiler aggregates your functions; you hand the framework a factory for the enclosing class. Here it is with Hilt:
import android.app.Application
import androidx.appfunctions.service.AppFunctionConfiguration
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
@HiltAndroidApp
class AntarApp : Application(), AppFunctionConfiguration.Provider {
@Inject lateinit var deviceFunctions: DeviceFunctions
override val appFunctionConfiguration: AppFunctionConfiguration
get() = AppFunctionConfiguration.Builder()
.addEnclosingClassFactory(DeviceFunctions::class.java) { deviceFunctions }
.build()
}You can ship a function disabled and flip it on when a feature/entitlement unlocks. The compiler generates an Ids constant per function (e.g. DeviceFunctionsIds.GET_BATTERY_INFO_ID):
import androidx.appfunctions.AppFunctionManager
suspend fun enableBatteryInfo(context: Context) {
AppFunctionManager.getInstance(context).setAppFunctionEnabled(
DeviceFunctionsIds.GET_BATTERY_INFO_ID,
AppFunctionManager.APP_FUNCTION_STATE_ENABLED
)
}Declare the default state on the annotation itself with @AppFunction(isEnabled = false, isDescribedByKDoc = true).
Before you wonder why Gemini can't see it, confirm the OS indexed it:
adb shell cmd app_function list-app-functions | grep --after-context 10 <your.package.name>If your function shows up here, the provider side is done.
Today the caller is Gemini. That integration is rolling out on Android 16+ (Galaxy S26, Pixel 10) with an Early Access Program open — so you build and register your functions now, and Gemini calls them on supported devices. The public docs focus on this provider side (the code above), which is the part you own.
The natural first candidates are the clean, typed "do one thing" functions you probably already have in a repository layer. They map almost one-to-one onto @AppFunction:
| Your app | Functions you'd expose | What the user can now say |
|---|---|---|
| 📝 Notes/tasks | createTask, searchTasks |
"Add a task to call the dentist" |
| 🔋 Device-info | getBatteryHealth, getStorageUsage |
"How's my battery health?" |
| 🖼️ Gallery | searchPhotos, sharePhoto |
"Find my photos from Goa" |
| 🚕 Rides | bookRide, getEta |
"Book a ride home" |
You're not building a chatbot. You're exposing the actions your app already does, and letting the phone's assistant trigger them.
Does my app need the CAMERA / network / special permissions to use this? No special permission to declare functions. The function runs as your app, with whatever access your app already has.
Is this the same as App Actions / App Shortcuts?
AppFunctions is the modern successor. App Actions used shortcuts.xml + built-in intents to map voice phrases to features; AppFunctions exposes typed, agent-discoverable functions instead.
Can I use this without Hilt?
Yes — Hilt is just how the sample provides the factory. You implement AppFunctionConfiguration.Provider and return the enclosing-class factory however you construct it.
Will it run on my non-Pixel/non-S26 phone? You can build/register against the API on Android 16+, but the Gemini-calls-your-function loop is private preview on those flagships for now.
- Overview of AppFunctions — Android Developers
- Add the AppFunctions API to your app — the code above is from here
- AppFunctions sample (ChatApp) — github.com/android/appfunctions
- androidx.appfunctions release notes
- The Intelligent OS — making AI agents more helpful (Feb 2026)
- Top AI on Android from Google I/O '26 (May 2026)
- ADK for Kotlin and Android — announcement