How to Structure Your Android App to Work With AI Agents
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 logicThe 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.