AI

How to Structure Your Android App to Work With AI Agents

Ahmad Najar Ahmad Najar 4 min read

Building for Agents Is an Architecture Decision, Not a Feature Bolt-On

If your app's business logic already lives buried inside ViewModels or, worse, inside Composables, making it agent-callable means untangling that first. The good news: if you've already been following clean architecture principles — domain layer separated from UI, use cases as the entry point for business logic — most of the hard work is already done. Here's how to structure things so AppFunctions, ADK agents, and on-device inference slot in cleanly instead of requiring a parallel codebase.

1. Put your use cases where both the UI and an agent can call them

The core principle: an AppFunction should call the exact same domain-layer use case your ViewModel calls — never a separate, parallel implementation. If "create a task" logic exists in two places, one for the UI and one for the agent, they will drift out of sync the first time either one gets a bug fix the other doesn't.

// domain/CreateTaskUseCase.kt — the single source of truth
class CreateTaskUseCase(private val repository: TaskRepository) {
    suspend operator fun invoke(
        title: String,
        dueDateTime: LocalDateTime?,
        location: String?
    ): Task = repository.createTask(title, dueDateTime, location)
}

// presentation/TaskViewModel.kt — UI calls it
class TaskViewModel(private val createTask: CreateTaskUseCase) : ViewModel() {
    fun onCreateTaskClicked(title: String) = viewModelScope.launch {
        createTask(title, dueDateTime = null, location = null)
    }
}

// ai/TaskAppFunctions.kt — the agent calls the same use case
class TaskAppFunctions(private val createTask: CreateTaskUseCase) {
    @AppFunction(isDescribedByKDoc = true)
    suspend fun createTask(
        appFunctionContext: AppFunctionContext,
        title: String,
        dueDateTime: LocalDateTime? = null,
        location: String? = null
    ): Task = createTask.invoke(title, dueDateTime, location)
}

This is exactly why Clean Architecture's separation of domain logic from the UI layer pays off here — the AppFunctions layer becomes a thin adapter, not a second implementation.

2. Write your KDoc like it's the API contract, because it is

With isDescribedByKDoc = true, the AppFunctions library generates the schema an agent uses to understand what your function does and how to call it — straight from your documentation comments. Vague KDoc produces a vague, unreliable tool description. Treat it with the same care you'd give a public API's documentation, because for an agent, it effectively is one:

/**
 * Creates a new task or reminder with a title, due time, and location.
 *
 * @param title The descriptive title of the task (e.g., "Pick up my package").
 * @param dueDateTime The specific date and time when the task should be completed.
 * @param location The physical location associated with the task (e.g., "Work").
 */

Specific examples in the parameter descriptions genuinely help the agent map natural language onto the right arguments — this isn't just style, it directly affects call accuracy.

3. Design functions around outcomes, not UI actions

A common mistake: exposing functions that mirror button clicks one-to-one instead of the actual outcome a user wants. "Tap the save button" isn't a meaningful agent action; "create a task with these properties" is. Model your AppFunctions around what a user is trying to accomplish, independent of how your UI happens to be laid out — this also tends to produce a cleaner domain layer even for your human-facing UI.

4. Treat destructive or high-stakes actions differently

Not everything should be equally easy for an agent to call unattended. A function that deletes data, sends a message, or spends money deserves extra scrutiny — require explicit confirmation in the flow, scope the permission tightly, or simply don't expose it as an AppFunction at all if the risk of an agent mis-triggering it outweighs the convenience. This is the same instinct as designing any external API: assume it will eventually be called with unexpected or malformed input, and design defensively.

5. Decide your on-device vs. cloud inference boundary up front

If you're using ADK for Android to embed actual agent reasoning in your app, decide deliberately which parts run locally via Gemini Nano/ML Kit GenAI APIs and which route to cloud Gemini. A reasonable default: privacy-sensitive or latency-sensitive sub-tasks (summarizing content already on the device, quick classification) stay local; complex multi-step reasoning or anything needing broader context routes to the cloud model acting as an orchestrator. Document this boundary explicitly in your architecture — it's a decision that affects privacy posture, offline behavior, and cost, not just performance.

6. Keep your Compose UI agent-friendly too, for Computer Control fallback

Not every interaction will go through AppFunctions — Computer Control means some agents will still drive your app by reading its screen directly. The best defense here is just good accessibility practice: meaningful contentDescriptions, proper semantic roles, and predictable, consistent UI structure. An app that's already accessible to screen readers is, not coincidentally, an app that a screenshot-reasoning agent can navigate more reliably too.

7. Version and test your AppFunctions like any other public contract

Once an agent (or a user's expectations, shaped by an agent) depends on a function's exact signature and behavior, changing it is a breaking change in the same way modifying a public REST API is. Use adb shell cmd app_function list-app-functions to verify what's actually registered on-device, and test against Android's sample agent app before assuming your functions behave the way your KDoc claims they do.

A minimal reference structure

com.app/
├── domain/            // use cases — the single source of truth for business logic
├── data/               // repositories, network, local storage
├── presentation/       // ViewModels, Compose UI
└── ai/
    ├── appfunctions/    // thin AppFunction adapters calling domain use cases
    └── agents/          // ADK agent definitions, on-device/cloud routing logic

The underlying principle

None of this requires a fundamentally different architecture than good Android apps already use — it requires actually having one. Apps with business logic tangled into ViewModels or UI code will find agent integration painful no matter which specific API they reach for. Apps with a clean domain layer will find that AppFunctions, ADK, and whatever comes next are thin, almost mechanical additions on top of work they'd already done for entirely separate reasons.

AI