Updated for Android 17 · API 36 · May 2026
Gemini Intelligence · AppFunctions · Kotlin

Android 17 Just Killed the App Icon.
Here's What Replaces It.

A developer's guide to AppFunctions — the architecture shift that separates the apps Gemini calls from the apps it ignores

May 2026 15 min read Android 17 · Kotlin · AppFunctions

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.

In 30 seconds — what this article covers
Unpopular opinion

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.

0
UI interactions needed to trigger an AppFunction
100%
On-device execution — data never leaves the phone
Engagement surface even when your app isn't open

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.

How the indexing actually works

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

build.gradle.kts (app) Kotlin DSL
// 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"
}
KSP, not KAPT

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.

features/workout/WorkoutFunctions.kt Kotlin
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.

The Dual-Entry Architecture — Two callers, one domain layer
Compose UI
User taps button → ViewModel
or
AppFunction
Gemini calls directly
UseCase → Repository
Shared domain layer — single source of truth
Result (sealed class)

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.

Senior dev move

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.20.95
start(d, t) startWorkout(distanceKm, workoutType) ~0.10.92
book(x) bookAppointment(doctorName, clinicLocation, timeSlot) ~0.150.97
msg(to, text) sendMessage(recipientPhoneNumber, messageBody) ~0.30.94
order(item, addr) placeOrder(productName, deliveryAddressLine1) ~0.20.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:

1

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.

2

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.

3

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.

Pitfalls Checklist — before you ship
Parameters named p1, data, type — Gemini can't match these
Full noun parameters: recipientPhoneNumber, distanceKm
Business logic inside the AppFunction class itself
AppFunction delegates 100% to UseCases — no logic, just translation
Long-running operations without suspend — blocks the calling thread
All non-trivial functions are suspend — coroutines are fully supported
No KDoc on functions — the manifest description is empty or generic
KDoc comments on every function — they directly improve intent matching
Sensitive actions (purchases, deletes) without requiresConfirmation
Irreversible actions flagged: @AppFunction(requiresConfirmation = true)
Assuming UI automation is a permanent fallback — it isn't
AppFunctions are the permanent solution — UI automation is the bridge

What to Ship This Sprint

Your move this week

Four steps. One sprint. Done.

Drop your AppFunction name in the comments — verb-noun pair, what it does. I'll review the first ten and give feedback on naming confidence.

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.

P.S.
If you're reading this after the summer 2026 Gemini rollout, go to Settings → Apps → [Your App] → Agent Skills on any Pixel or Galaxy device. If your app doesn't appear there, this article is exactly why. It's not too late — but the index hierarchy is already forming.
Android Developer · 10 years in the field

Writing Android since the Jelly Bean era. Currently obsessed with agentic architecture and the intersection of AI and mobile. Opinions are my own — and I'm willing to defend them.

#AndroidDev #Kotlin #GeminiAI #AppFunctions #Android17 #MobileArchitecture #GenerativeAI #AgenticFuture #JetpackCompose #MCP