Your app's UI just became optional. Not deprecated, not replaced — optional. Android 17 lets Gemini bypass your beautifully crafted screens completely, calling your app's capabilities in the background without ever launching a single Activity. And here's the uncomfortable truth: if you haven't made your app "agent-ready," Gemini will route users to a competitor who has. Not with a warning. Not with a migration notice. It'll just happen, silently, and you'll wonder why your DAU numbers are drifting.
I've been writing Android since the Jelly Bean days — long enough to have lived through AsyncTask, Fragments, RxJava, Coroutines, and the slow migration to Compose. Some shifts looked bigger than they were. This one looks exactly as big as it is. And the window to get ahead of it is right now, before the Pixel/Galaxy rollout this summer locks in the ecosystem hierarchy.
- AppFunctions let Gemini call your app without opening the UI — this is not a drill
- The right architecture: one
UseCase, two entry points (Compose UI + AppFunction) - How the KSP compiler generates a natural-language capability manifest the OS indexes
- Why naming is silently killing most integrations — and the exact patterns that fix it
- The
requiresConfirmationflag, the Agent Simulator, and the common pitfalls list - What to ship in this sprint to be ready before the summer rollout
If you're still optimizing for time-to-first-screen instead of time-to-task-completion, you're building for 2023. Android 17 doesn't deprioritize bad UIs — it renders them irrelevant. Your Compose animations won't save you if Gemini doesn't know your app exists as a function.
The Real Shift Nobody Is Talking About
Google is officially calling Android an Intelligence System, not an operating system. That's not marketing copy — it's an architectural statement. Gemini is no longer a chatbot you open in an app. It's the OS layer that decides which app gets called, what data it needs, and whether it even bothers to open your UI. The metric that used to matter — "Is the user in my app?" — is being replaced by one that's harder to optimize for and more honest: "Did my app help the user finish the task?"
Android 17 handles this through two tracks. The first is UI Automation — zero code changes, Gemini navigates your app visually using accessibility APIs. It's the free baseline, and it's fragile (it breaks on every redesign). The second is AppFunctions — structured, intentional, on-device function calls that Gemini can discover, understand, and execute directly. The apps that invest in the second track are the ones that will own the agent layer.
What AppFunctions Actually Are (And Aren't)
Before we write a line of code, get the mental model right — because I've seen intelligent developers confuse AppFunctions with three things they are not: they are not App Actions (the old intent-based approach), they are not deep links with natural language wrappers, and they are absolutely not annotated methods you bolt onto your existing ViewModel.
AppFunctions are self-describing, OS-indexed, on-device tools that Gemini can discover and call autonomously. The closest analogy in the current ecosystem is the Model Context Protocol (MCP). MCP lets server-side LLMs connect to tools via HTTP. AppFunctions does the same thing for your Android app, locally, with the OS acting as the broker. Your app becomes a microservice that Gemini can call. The user never sees your launcher icon — but your service still solved their problem.
When you annotate a function with @AppFunction, the KSP compiler generates an appfunctions.json manifest that's merged into your APK's assets at build time. The OS reads this manifest on install and indexes your capabilities. Gemini queries that index when resolving user intent. This is why the annotation alone isn't enough — the manifest content, which is derived from your identifier names and KDoc comments, is what Gemini actually reads.
Building It: Architecture First, Code Second
The developers who will regret AppFunctions are the ones who treat it as a feature to tack on. The right approach: AppFunctions are a separate entry point into your domain layer, sitting alongside your UI layer, sharing the same business logic, touching none of the same ViewModel concerns.
Step 1: Dependencies
// AppFunctions core + KSP compiler
// Note: alpha APIs can change — track releases at d.android.com/appfunctions
dependencies {
implementation("androidx.appfunctions:appfunctions-core:1.1.0-alpha05")
ksp("androidx.appfunctions:appfunctions-compiler:1.1.0-alpha05")
}
// KSP plugin — required in plugins block
plugins {
id("com.google.devtools.ksp") version "2.0.21-1.0.28"
}
AppFunctions uses KSP exclusively. If you're still on KAPT for other libraries, you'll need both processors. Don't mix them on the same class — annotation processing order issues from that combination are genuinely painful to untangle.
Step 2: Write the AppFunction class
The class doesn't know about ViewModels, UI state, or Compose. It only knows the domain layer — specifically, a UseCase or Repository. The @AppFunction annotation tells the KSP compiler to include this function in the generated appfunctions.json. For sensitive or irreversible operations, requiresConfirmation = true forces a user confirmation step before Gemini executes the function.
import androidx.appfunctions.AppFunction
import androidx.appfunctions.AppFunctionContext
/**
* AppFunction boundary: translates an agent call into a domain action.
* Contains NO business logic — delegates entirely to the UseCase layer.
* This is the same UseCase your ViewModel already calls.
*/
class WorkoutFunctions @Inject constructor(
// Constructor injection — same UseCase your ViewModel already has
private val startWorkoutUseCase: StartWorkoutUseCase,
private val purchaseUseCase: PurchasePlanUseCase
) {
/**
* Starts a workout tracking session.
* "Track my 5km run" → Gemini resolves to this function.
*
* Parameter names are read by Gemini. Be explicit.
* "distanceKm" > "distance" > "d". Always.
*/
@AppFunction
suspend fun startWorkout(
context: AppFunctionContext,
distanceKm: Double,
workoutType: String // "run", "cycle", "swim" — Gemini infers this
): String = when (val result = startWorkoutUseCase.execute(distanceKm, workoutType)) {
is WorkoutResult.Success -> "Tracking your ${distanceKm}km ${workoutType}. GPS locked."
is WorkoutResult.GpsUnavailable -> "GPS signal weak. Try moving outside first."
is WorkoutResult.AlreadyRunning -> "You already have an active session running."
}
// requiresConfirmation = true pauses execution for user approval
// Use this for purchases, deletions, and any irreversible action
@AppFunction(requiresConfirmation = true)
suspend fun purchaseTrainingPlan(
context: AppFunctionContext,
planName: String,
priceInRupees: Double
): String {
val result = purchaseUseCase.execute(planName, priceInRupees)
return if (result.success) "${planName} purchased for ₹${priceInRupees}."
else "Purchase failed: ${result.errorMessage}"
}
}
Step 3: The dual-entry architecture
This is the diagram I wish someone had shown me during developer preview. Your codebase now has two entry points into the same domain layer. Both are first-class citizens. Neither owns the other.
User taps button → ViewModel
Gemini calls directly
Shared domain layer — single source of truth
The Compose UI goes through the ViewModel as it always has. The AppFunction skips the ViewModel entirely and calls the UseCase directly. Both produce the same result because they share the same business logic. The ViewModel was always just an adapter between the UI lifecycle and the domain — it was never part of the domain itself. This architecture makes that distinction explicit.
Write unit tests for your AppFunction classes the same way you test UseCases — inject mock repositories, assert return strings. The AppFunction runtime is a thin wrapper. Your logic is fully testable without any OS involvement, no instrumented tests required.
The Naming Problem That Will Silently Kill Your Integration
I'm giving this its own section because the failure mode is invisible. If Gemini can't understand what your function does from its name and parameter names, it won't call it. It won't throw an error. It won't log a warning. It will just not call it — and your competitor whose function is named better will get the traffic. You'll never know you were an option.
The KSP compiler generates natural language descriptions for the appfunctions.json manifest from your identifiers and KDoc comments. This is both the elegance and the responsibility. The AI reads the manifest. You are, in effect, writing an API contract for an LLM. Write for the model, not for your autocomplete.
| Bad — Gemini ignores this | Good — High intent match | Confidence |
|---|---|---|
| doAction(p1, p2) | sendPayment(recipientName, amountInRupees) | ~0.2 → 0.95 |
| start(d, t) | startWorkout(distanceKm, workoutType) | ~0.1 → 0.92 |
| book(x) | bookAppointment(doctorName, clinicLocation, timeSlot) | ~0.15 → 0.97 |
| msg(to, text) | sendMessage(recipientPhoneNumber, messageBody) | ~0.3 → 0.94 |
| order(item, addr) | placeOrder(productName, deliveryAddressLine1) | ~0.2 → 0.91 |
The rule is simple: function names are verb-noun pairs (startWorkout, sendMessage, bookAppointment). Parameter names are full, unambiguous nouns (recipientEmailAddress not email, deliveryAddressLine1 not addr1). Add KDoc comments — the compiler includes them in the manifest and they significantly improve intent matching on complex or domain-specific operations.
Tooling: The AppFunction Inspector in Android Studio Ladybug
Android Studio's 2026 release ships with an AppFunction Inspector that deserves more attention than the docs give it. Three features that will save you real debugging time:
The Agent Simulator
Type a query exactly as a user would say it to Gemini. The simulator shows which AppFunction gets matched, with what parameter values, and what the resolved return looks like. This is the fastest way to catch naming failures before they ship — use it before every PR that touches an AppFunction class.
The Manifest Preview
Shows the generated appfunctions.json exactly as the OS will index it. Read it as Gemini would. If you can't tell what your function does from the manifest alone — without looking at the source code — neither can the model. Rewrite until the manifest is self-explanatory.
Parameter Confidence Scores
The simulator scores parameter resolution confidence per query. If distanceKm resolves at 0.6 from "run 5 kilometers," iterate on the name until you hit 0.9+. This is empirical naming feedback. Use it obsessively on your first few integrations — the patterns you learn will carry across every function you write.
Privacy and Security: What Google Actually Got Right
The first question every PM asks when you propose this is some variation of "Wait, that sounds like a massive privacy risk." It's a fair question. The answer is more reassuring than you'd expect, and understanding the architecture helps you make the case.
Every AppFunction executes on-device, in your app's process, with your app's permissions. Not on Google's servers. Not in the cloud. User data involved in a function call never leaves the device. This matters enormously for GDPR compliance, data residency requirements, and the increasingly sophisticated privacy expectations of users in regulated markets.
For sensitive operations — purchases, data deletion, message sending — use @AppFunction(requiresConfirmation = true). The runtime pauses execution and surfaces a native confirmation UI before proceeding. Users can review the capability manifest at any time in device settings and can revoke per-app AppFunction permissions individually. The permission model is more granular than most notification permission systems we've shipped against.
The Business Case (The One-Paragraph Version for Sprint Planning)
Gemini will be the default interaction layer for a meaningful percentage of Android users within 18 months. Apps that expose AppFunctions become first-class citizens in that layer. Apps that don't get either ignored — if another app covers the use case — or handled through UI automation that breaks on every redesign. The investment is one well-architected function class, one afternoon in the Agent Simulator, and one set of unit tests. The return is a new engagement surface that requires no app opens, no onboarding friction, and no competing for home screen attention.
"This is about not being the developer equivalent of a restaurant that refused to list on delivery apps in 2018. It felt optional then too."
Common Pitfalls — The Checklist
Before any AppFunction goes to code review, run it against this list. I've seen every one of these in the wild during the developer preview period.
p1, data, type — Gemini can't match theserecipientPhoneNumber, distanceKmsuspend — blocks the calling threadsuspend — coroutines are fully supportedrequiresConfirmation@AppFunction(requiresConfirmation = true)What to Ship This Sprint
Four steps. One sprint. Done.
-
1Identify your top 3 user actions by retention data — the things your best users do most. Those are your first AppFunction candidates. Don't expose everything; expose the three tasks a user would most naturally delegate to Gemini.
-
2If that logic lives in a ViewModel, extract it into a UseCase first. This pays dividends regardless — cleaner architecture, better testability — and gives AppFunctions a clean hook without touching your UI layer.
-
3Write one AppFunction class per feature. Semantic names. KDoc comments. Constructor injection. Use the Agent Simulator in Android Studio Ladybug to verify intent matching before the PR goes to review. Aim for 0.9+ confidence scores on your primary queries.
-
4Register for the AppFunctions Early Access Program. Google is actively prioritizing EAP apps for Gemini feature placement and index priority during the Pixel/Galaxy summer rollout. Getting in early has concrete discoverability benefits that are hard to recover after launch.
I have been around long enough to have been wrong about paradigm shifts before. I was late to Compose. I underestimated Coroutines at first. I'm telling you now: don't be late to this one. The "headless app" era is in developer preview today and rolling to Pixel and Galaxy devices this summer.
The developers who treat AppFunctions as a first-class architectural concern in the next two months will look prescient in eighteen. The ones who add it as an afterthought — bolted onto the ViewModel, with parameters named p1 and p2 — will spend those same eighteen months in emergency refactors wondering why their app doesn't show up when Gemini handles their core use case.
Stop building silos. Start building capabilities. The architecture is ready. The tooling is ready. The only question is whether you are.