Ask an AI assistant to help you prepare for a developer conference. A text response can give you a checklist. An interactive response can let you tick an item, inspect the venue, and ask the assistant to update your plan using what you have already completed.
That second interaction is what we built in PocketCommunity, the latest app in the Android Engineers Android + AI Cookbook.
The app reads published developer-event listings from DevEarth. Gemini composes an interface from a restricted set of components. A companion server validates that composition, and the AndroidX A2UI renderer displays it using native Jetpack Compose components.
The interesting engineering problem starts after the first card appears: how does a tap become agent context, and how do we keep the next generated screen consistent with the user’s current state?
What actually generates the screen?
A2UI is a protocol, not an inference provider. Its renderer does not require a Gemini key. It can render a developer-authored message just as it can render one produced by an agent.
PocketCommunity uses Gemini as the producer, so our server needs a Gemini credential. The Android app needs the server URL. These are different responsibilities.
In this implementation, Gemini returns:
{
"text": "Here is a preparation checklist for your event.",
"components": [
{ "id": "root", "component": "Column", "children": ["heading"] },
{ "id": "heading", "component": "Text", "text": "Before you go" }
]
}This is a minimal illustrative composition, not a complete event response. The server validates the components and wraps them in A2UI messages. Gemini does not generate Kotlin, and the app does not compile code received from the model.
The official Compose A2UI guide describes the separation between the rendering engine and the catalogs that map protocol components to native implementations. Our app adds the Gemini producer, event-data boundary, and application-specific validation around that renderer.
Gemini chooses the composition; the server validates it; Android renders registered native components.
In the shipped preview, no configured endpoint means setup—not a simulated AI response. Deterministic messages remain useful in instrumentation tests, where we explicitly inject a fake repository. They are not a runtime fallback.
Source: PocketCommunity at the article’s pinned revision.
A surface has structure and state
Think about a single preparation checklist. It needs a component tree, but it also needs values that can change when the user interacts.
A2UI separates these concerns. In PocketCommunity, the server constructs three messages for each new response surface:
createSurfaceidentifies the surface and its catalog.updateDataModelsupplies initial values.updateComponentssupplies the component graph.
Here is a compact example matching the pinned implementation’s wire format:
[
{
"version": "v0.9",
"createSurface": {
"surfaceId": "preparation-1",
"catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json",
"sendDataModel": true
}
},
{
"version": "v0.9",
"updateDataModel": {
"surfaceId": "preparation-1",
"path": "/",
"value": { "ready0": false }
}
},
{
"version": "v0.9",
"updateComponents": {
"surfaceId": "preparation-1",
"components": [
{ "id": "root", "component": "Column", "children": ["pack"] },
{
"id": "pack",
"component": "CheckBox",
"label": "Pack a charger",
"value": { "path": "/ready0" }
}
]
}
}
]pack identifies a component. /ready0 identifies a value in the surface’s data model. They are not interchangeable.
When the user checks the item, the bound value changes. The UI can reflect that change without another model call.
A version detail matters here: the current Android guide describes specification 0.9.1 support, while this pinned alpha implementation uses v0.9 and the v0_9 catalog URL in its messages. Keep the tested dependency and protocol values together rather than editing version strings to match a documentation heading.
The sample currently receives a complete HTTP response containing the message batch. Processing multiple messages does not mean we have implemented token streaming or an SSE transport.
One response batch becomes a surface through the parser, processor, and Material catalog.
The catalog is your UI contract
The server allows eight component types:
const allowed = new Set([
'Text', 'Column', 'Row', 'Card',
'Button', 'CheckBox', 'Divider', 'Image'
]);This is our sample’s subset, not the entire A2UI component vocabulary.
On Android, communityCatalog() configures the Material basic catalog. We supply a custom image implementation that recognizes only asset://community and renders bundled artwork. Generated output cannot turn that component into an arbitrary remote image request.
The catalog’s URL opener also rejects direct navigation. Website and directions actions go through application code, which resolves the official HTTPS link from a known event record.
This keeps the agent’s freedom specific: it can choose headings, group supported components, and offer permitted actions. The app still owns its component implementations and the effects those actions can trigger.
The conversation header, composer, setup dialog, and inspector are ordinary Compose UI. The response surfaces are rendered from A2UI messages. An agent-generated experience does not require handing the whole application shell to the model.
Keep inference outside the Composable
We use a small repository boundary:
interface CommunityRepository {
suspend fun discover(): List<CommunityEvent>
suspend fun ask(endpoint: String, request: AgentRequest): AgentReply
}
data class AgentReply(
val text: String,
val messages: List<String>,
)CommunityViewModel owns request coordination, conversation state, the message processor, and outgoing action handling. Compose observes state and sends user intent.
This is useful for a concrete reason: tests can replace the network repository while exercising the real native renderer and action flow. We do not need a live Gemini request to prove that a rendered button dispatches the expected event.
Request ownership also matters. A response from an old conversation must not appear after the user starts a new one. The implementation cancels its job and advances a generation counter on reset; after a repository call returns, it checks coroutine activity and whether the request still belongs to the current generation.
There is an important limit: the repository uses synchronous OkHttp calls on Dispatchers.IO. Cancelling the coroutine does not automatically establish immediate transport cancellation or stop provider computation. UI ownership and network cancellation are separate engineering concerns.
A checkbox-aware agent needs more than chat history
Suppose the user checks “Pack a charger” and then taps Update my plan.
If the next request contains only “Update my plan,” the agent has no reliable description of what changed. Even sending the visible conversation text is insufficient: checkbox values are not necessarily written into that text.
PocketCommunity forwards:
The current request and selected event ID.
Recent conversation text.
The actual action name, originating surface/component IDs, and context.
Previous component trees.
Snapshots of bound surface values.
A button’s generated action looks like this:
{
"event": {
"name": "refine",
"context": { "eventId": "published-event-id" }
}
}The ID above is a placeholder; real actions must reference a current published event.
The ViewModel resolves that ID before proceeding. venue, prepare, and refine become agent requests. website and directions use known listing URLs. save remembers the event for the session and explicitly does not register the user.
The request context is bounded: up to 12 text turns, three previous component trees, and six surface snapshots. These are sample limits, not a complete token-budgeting or conversation-memory system.
A checkbox changes local state; a later agent request carries the state and the originating action.
The bug that exposed the state boundary
During a physical-device check, Gemini correctly responded that one of four items was complete. But the newly rendered checklist showed every checkbox unchecked.
The agent had received the state. The UI initialization discarded it.
Our server was initializing every checkbox in the new surface to false. The fix was to carry the relevant values into the new surface’s initial data model:
const prior = new Map(
previousChecklist.map(item => [item.label, item.checked])
);
const value = {};
for (const component of reply.components) {
if (component.component === 'CheckBox') {
value[component.value.path.slice(1)] =
prior.get(component.label) === true;
}
}
On refine, the server finds the originating surface’s earlier components and values, then matches exact labels. An existing item can retain its checked value even if its binding path changes. A new or renamed item starts unchecked.
This is an application-level migration policy, not automatic A2UI persistence. Exact-label matching is also a deliberate preview limitation. Stable domain item IDs would be stronger if the agent edits wording, two items share text, or older context falls outside the retained window.
The corrected flow was verified on the phone: the generated progress text and the new checkbox state agreed.
Actual PocketCommunity output after refinement: one item remains checked. Completion is self-reported, not externally verified registration.
Valid JSON is only the first check
A syntactically valid response can still reference missing components, create a cycle, invent an action target, or describe a misleading event.
The sample validator checks component names and properties, ID uniqueness, graph references, cycles, reachability, graph depth, action names, published event IDs, checkbox paths, and output limits. Android then checks the expected message envelope and surface identity before passing messages to the official processor.
These checks answer different questions from factual evaluation. A valid Text component can still contain an incorrect venue or an invented agenda. Event facts must be compared with the source feed. A checked registration item means the user marked it complete—not that our application verified a ticket purchase.
One instructive gap remains in the published baseline: two independent checklist components can bind to the same /ready0 path. Their component IDs can be unique while their values remain shared.
That is the practical exercise in our PocketCommunity codelab. You write a failing test, add a per-surface duplicate-path policy, and rerun the suite. The policy is specific to independent checklist tasks; shared bindings can be intentional in other interfaces.
What we verified—and what remains
The published app checkpoint passed debug and release builds, two JVM tests, three physical-device instrumentation tests, and ten Node tests. The instrumentation tests use a fake repository with the real renderer. Separately, live Gemini generation was exercised on a OnePlus DN2101 running Android 13: event discovery, venue details, preparation, and checkbox-aware refinement.
That distinction is worth preserving in your own reports. A passing UI test is not evidence of successful inference. A successful inference is not a comprehensive usability evaluation.
This remains a learning preview. Conversations are not restored after process death; saves are session-only; embedded maps and streaming transport are not implemented. The companion service binds to localhost and does not provide production authentication, per-user quotas, or rate limiting. The A2UI dependencies are alpha and pinned together.
Build it, then improve it
The source includes the Android app, companion server, tests, setup instructions, and diagrams:
git clone https://github.com/AndroidEngineers/android-ai-cookbook.git
cd android-ai-cookbook
git switch --detach 33ff08b56b3e5f00240c99cf2848f0e35bdde998
git switch -c learning/pocketcommunity
cd a2ui
Open a2ui/ in Android Studio. The Gemini key belongs in server/.env; the phone’s Setup field takes the agent endpoint. The README covers Node setup, the pinned Java toolchains, and ADB reverse for a connected phone.
Choose the learning route that fits how you work:
Follow the codelab: configure the app, trace real messages, implement the validator improvement, and verify it with tests.
Study the roadmap: 12 units covering catalogs, protocol state, request ownership, action feedback, validation, and an independent capstone.
Explore the Android + AI Cookbook: PocketCommunity joins PocketCards, PocketCook, PocketChat, and PocketStories in our collection of focused Android learning apps. Each sample documents its own readiness and limitations.
If you build on PocketCommunity, show us the interaction after the first response: what did the user change, what did the agent receive, and how did the next native screen preserve that meaning?
That is where an impressive generated card becomes an Android engineering project you can explain and test.






