Async A2UI Turns AI-Generated Flutter UIs into Replayable, Cacheable Assets
Actually, we've talked about Google and Flutter's A2UI quite a few times, but one problem has been mentioned before: LLMs generate interfaces too slowly. For example, a single Generative UI request goes through:
The model reads context, understands business data, calls tools, generates A2UI JSON, and then Flutter converts the JSON into Widgets. No matter how fast the model is, it still takes tens of seconds or even minutes of waiting.
So Flutter's official team made a scenario optimization for this: If enough information is available to generate the interface before the user opens the App, why not generate it in advance?
The official team provided a case study called Commis, a scenario for an app used by a catering team. Firestore stores catering job data, such as what time an event is on a certain day, the address, and how many meals to prepare.
The typical Generative UI flow used to be: after the user opens the page, the client sends this information to the Agent, and then Gemini generates a corresponding UI card on the spot.
But now, Async A2UI moves this generation timing forward. For example, once a job in Firestore changes, it directly triggers a background Cloud Function. The Cloud Function hands the latest job data to the AI, letting the model generate the corresponding A2UI, and the generated result is written back to Firestore.
At this point, as soon as the user actually opens the App, the Flutter client can directly read out and render the already generated A2UI. That is, based on user data, we can pre-generate pages in certain scenarios.
Following this line of thought, could we let AI pre-generate some personalized UIs or interactions based on the user's own data for certain events or holiday scenarios?
From an architectural perspective, this is actually very similar to a Materialized View in databases:
- The job in Firestore is the raw business data.
- A2UI can be seen as a UI projection computed from this business data.
- When the raw data changes, the UI is recomputed in the background.
- When the user actually needs to read it, the pre-computed result is used directly.
The significance of this step for Generative UI scenarios is actually much greater than simply "caching a piece of JSON." Thinking this way, it even starts to change the role of A2UI. A2UI is no longer just a transient intermediate message that exists briefly during the LLM's real-time output; it starts to become a piece of UI data that can be saved, transmitted, restored, and replayed.
For example, in the past, we easily thought of the Generative UI chain as:
User → LLM → A2UI → Flutter Widget
The user makes a request, the model thinks, the model generates UI, Flutter renders it. But Async A2UI actually splits this chain into two lifecycles. The background lifecycle becomes:
Business Data Change → Agent → A2UI → Storage
Meanwhile, the user side becomes:
App Open → Storage → A2UI Runtime → Flutter Widget
These two processes can happen at completely different times, even on different devices and servers. At the same time, the UI can be constrained by the App developer's design specifications and limited by the Flutter-side Widget Catalog, preventing it from becoming completely dynamic and out of control.
It means you could even save a model-generated interface to Firestore, SQLite, Redis, object storage, or even a CDN.
Just like the _transport.addChunk(feeds) in the code below. On the surface, it just stuffs a string from Firestore in. But actually, when Flutter GenUI normally connects to AI, the A2uiTransportAdapter continuously receives text chunks streamed from the model's output, then hands them to the A2uiParserTransformer to parse into A2UI Messages, after which the SurfaceController updates the UI:
Future<void> _initAgent() async {
// This repository is a class I use to hide away queries to Firebase. It's
// really just grabbing values and providing a stream.
final repository = context.read<FirestoreRepository>();
String? feeds;
try {
// 1. Fetch active jobs and wait for their cached feed messages
final jobs = await repository.getJobs();
final feedFutures = jobs.map((job) => repository.getFeedMessage(job.id));
final results = await Future.wait(feedFutures);
// 2. Combine the non-empty cached A2UI messages
feeds = results
.map((r) => r?.trim() ?? '')
.where((r) => r.isNotEmpty)
.join('\n\n');
} catch (e) {
debugPrint('Error initializing agent: $e');
}
// 3. Initialize the agent session, passing the cached messages
_agentService = FirebaseAILogicService(
repository: repository,
catalog: _catalog,
cachedMessages: feeds,
);
// 4. Feed the cached messages directly to the transport adapter
if (feeds != null && feeds.isNotEmpty) {
_transport.addChunk(feeds);
}
setState(() => _isWaiting = false);
}
So actually, Async A2UI didn't develop a separate "Cached Renderer." It just takes the cached string read from Firestore and directly re-feeds it into the original Transport.
That is, the A2UI Runtime doesn't need to care where this piece of A2UI comes from. It might be something the AI just spat out this moment, something a backend server generated yesterday, something saved in local SQLite for a week, or even a test fixture handwritten by a developer. As long as the message satisfies the A2UI protocol, the subsequent parsing and Surface building process is exactly the same.
This effectively gives A2UI a replayable characteristic.
A2UI itself contains messages like createSurface, updateComponents, updateDataModel, and deleteSurface. It's more intuitive to understand these messages as a series of operations on the UI state:
- Create a Surface
- Fill components into the Surface
- Update the DataModel
- Delete a Surface
That is, as long as you save this series of inputs and replay them once, you can restore the UI. This already smells a bit like a UI Event Log, full of new imaginative possibilities, right?
I feel that if we continue along this line of thought, another more interesting architecture easily emerges: Cache the initial Surface, then perform real-time incremental updates. For example:
- An AI Dashboard had its structure generated last night. When the user opens the App in the morning, the cached Surface can appear instantly.
- Simultaneously, the client starts the Agent, letting the model read today's new data. If only a few numbers have changed, it sends
updateDataModel. - If a certain block of UI needs to change, it sends
updateComponents. - The user sees the old version appear instantly, which is then quickly refreshed to the latest state.
This process requires no release or update, and it can be flexibly controlled. I find this caching support quite interesting. Even Firestore can be replaced; you can define or choose your own Cloud Function.
Moreover, the cached A2UI read from Firestore isn't just sent to the Flutter Renderer; it's also handed to the Agent at the same time. So, for example, if the backend generated a card yesterday, and the user opens the App today, Flutter restores the card from the cache. Then the user says: "Change this event to next Monday." With context, the model knows which UI elements this event involves.
But this also introduces new problems. The UI State of Generative UI will need to become part of the Agent State. The overall state becomes much more complex:
- Generative UI needs to additionally save which Surfaces currently exist.
- What Components each Surface has.
- The current state of the DataModel.
- What the user just operated on.
- What the model has previously created.
- Whether the current UI was created by the current Agent Session.
The entire App's state could become quite coupled. The Flutter Runtime needs to restore the UI State, and the Agent needs to restore its awareness of the UI; both sides must correspond exactly.
And if the Surface becomes increasingly complex, this whole system is likely to further evolve with mechanisms like snapshots, revisions, event logs, and checkpoints. For example, save the current Surface Snapshot, while also saving the corresponding conversation revision that generated it, and then restore directly to that checkpoint when the Agent Session resumes.
Then new problems arise again. What if the cache becomes invalid? For example:
- At 10:01, the user modifies a job, and the background triggers AI to generate A.
- At 10:02, the user modifies it again, and the background starts generating B.
- The completion time of LLM requests is uncertain. If B comes back first and A comes back later, the old UI could completely overwrite the new UI.
Even after a Flutter App update, the Widget Catalog might change. The A2UI previously generated by the server might reference an old Component schema that the new version of the client no longer recognizes...
So, if it's really used in a production scenario, a piece of JSON is definitely not enough; all sorts of metadata must be complete.
From this perspective, what A2UI wants to do is getting closer and closer to a Generative UI Runtime. The LLM is responsible for decision-making and composition, A2UI is responsible for describing the UI, and the Flutter Runtime is responsible for safely mapping the description to real Widgets. The generation end and the rendering end no longer need to maintain a synchronous connection, or even be online at the same time. For example:
- Stable core parts like Navigation, Scaffold, payments, and login remain fixed code.
- Highly dynamic but predictable areas like recommendation cards, Dashboards, task summaries, and personalized Feeds use Async A2UI for pre-generation.
- Forms, tool panels, and temporary workflows that truly depend on the user's current language commands are handed over to the Agent for real-time generation.
Or perhaps only this is the correct direction for A2UI to land.