【文章翻譯】Speeding up generative UI with async A2UI

【文章內容使用 Gemini 2.5 Flash 自動翻譯產生】

原文:https://flutter.dev/blog/speeding-up-generative-ui-with-async-a2ui

使用非同步 A2UI 加速生成式 UI

生成式 UI (GenUI) 正在改變我建構使用者介面的方式。GenUI 允許 AI 代理程式在執行時使用基於 JSON 的訊息動態組合和更新使用者介面,而不是向每個使用者呈現一組靜態畫面。在 Flutter 中,genui 套件讓我可以即時渲染這些 AI 驅動的組件。

然而,建構代理應用程式會帶來一個熟悉的挑戰:啟動延遲

如果應用程式在啟動時依賴呼叫大型語言模型 (LLM) 來生成初始介面,使用者將會一直等待該模型生成回應。LLM 必須處理系統提示、進行函數呼叫以擷取資料庫記錄,並建構 UI 負載,所有這些都可能比使用者在導航到新畫面時習慣等待的時間更長。為了讓 GenUI 應用程式感覺即時且反應靈敏,我需要一種方法在使用者甚至打開應用程式之前預先生成和快取這些 UI 佈局。

在這篇文章中,我將向您展示一種透過快取 A2UI 訊息來預先生成使用者介面的技術,然後逐步說明如何取得這些訊息並在代理應用程式中無延遲地顯示它們。


非同步 GenUI 快取的架構

為了將其納入情境,讓我們看看我如何將這種快取技術應用於我的範例應用程式 Commis,這是一個用於商業廚房和餐飲團隊的智慧助理。使用者體驗的核心是一個對話式聊天介面,廚師可以與 AI 助理討論即將到來的餐飲工作,以協調食譜分配和賓客數量。但是,當廚師第一次打開應用程式時,他們不應該從頭開始聊天。相反,我希望代理程式提供預先生成的 UI,其中包含與每項工作相關的資訊和操作。

為了解決生成這個豐富的啟動視圖的延遲問題,我決定將初始 UI 組件的生成過程與客戶端應用程式的執行時生命週期分開。每當資料更改時,在後台非同步生成 UI,允許後端預先快取佈局,使其準備好在啟動時立即載入。

以下是這個後台生成流程的樣子:

Sequence diagram showing the asynchronous UI generation flow between Firestore, Cloud Function trigger, and Gemini API

當使用者打開 Flutter 應用程式時,我完全繞過 LLM,直接從資料庫中提取預先計算的 UI,並將其加入到我的對話歷史記錄和 genui 傳輸層。透過將這兩個階段分離,使用者可以獲得即時的啟動體驗,但我仍然保留了動態、AI 生成介面的絕對靈活性。讓我們看看我是如何將其組合在一起的。


1. 後端的非同步 UI 生成

解決方案的前半部分位於伺服器端,我正在使用 Firebase 函數中新的(且技術上是實驗性的!)Dart 支援與觸發器。每當餐飲工作記錄更改時,就會啟動一個函數。它會檢索更新的資料,向 Gemini 發送提示以建構 UI,並將產生的 JSON 寫入名為「feeds」的 Firestore 集合中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// functions/bin/server.dart

import 'package:commis_functions/src/content_generator.dart';
import 'package:firebase_admin_sdk/firebase_admin_sdk.dart';
import 'package:firebase_functions/firebase_functions.dart';

void main() {
final adminApp = FirebaseApp.initializeApp();
final firestore = adminApp.firestore();

runFunctions((firebase) {
// NOTE: Firestore triggers (like onDocumentWritten) are currently
// experimental in the Dart Cloud Functions SDK and are only supported
// when running within the Firebase Emulator Suite.
firebase.firestore.onDocumentWritten(
(event) async {
// Read the API key from the environment parameters
final apiKey = StringParam(
'GEMINI_API_KEY',
ParamOptions(
defaultValue: '',
label: 'Gemini API Key',
description:
'API key to use when accessing the Gemini Developer API',
),
).value();

final jobId = event.params['jobId'];
final jobData = event.data?.after?.data();

// Ensure we actually have post-write data to process
if (jobData == null) {
print('Could not find post-write job data.');
return;
}

final jobBuffer = StringBuffer();

// ...
// A whole bunch of lines that turn the job's properties into text for
// the agent.
// ...

try {
final generator = ContentGenerator(apiKey);
// Via a class that encapsulates the call to the LLM, generate A2UI.
final message =
await generator.generateFeed(jobData.toString());

if (message == noUiSentinel) {
// Delete the cached document if the event is no longer upcoming
await firestore.collection('feed').doc(jobId).delete();
print('No UI generated; deleting feed for $jobId.');
return;
}

// Cache the generated A2UI message in the feeds collection
await firestore.collection('feed').doc(jobId).set({
'message': message,
});

print('Wrote feed for $jobId.');
} catch (e) {
print('Error generating UI: $e');
return;
}
},
document: 'jobs/{jobId}',
);
});
}

在這個 ContentGenerator 類別中,我封裝了一個系統指令,其中包含我的應用程式的目錄(一系列關於可以生成哪些 UI 組件及其屬性的指令)、我選擇的模型(在本例中為 gemini-3.1-flash-lite),以及如果沒有要顯示的特定工作 UI 則返回其他邏輯。

我還給 LLM 一些嚴格的指示,說明不要包含什麼,事後看來,這讓我看起來像在對它大吼大叫!

1
2
3
**只回應 NavigationCard 或 "$noUiSentinel" 的 A2UI 訊息。什麼都不要。**
**不要包含任何對話文本。**
**不要包含任何關於您正在做什麼的解釋。**

因為這是一個簡單的演示範例,所以我對代理程式可以從目錄中建立哪些項目進行了嚴格的限制。不過,這並不是一個要求。您可以將整個目錄和說明提供給您的代理程式,讓它們盡情發揮創造力,或者縮減可用的選擇以保持嚴格的限制。

在這種情況下,如果餐飲工作即將到來(例如,在接下來的幾天內),Gemini 會回應一條簡潔的 A2UI 訊息,指示客戶端顯示地圖和導航詳細資訊。如果活動時間更長,它會返回我的標記字串 “NO GENERATED UI”,提示函數清除該工作的 feed。

有了這個,我就有一個充滿預先生成的使用者工作卡片的 Firestore 集合,這意味著我的應用程式已準備好顯示生成的 UI,而無需等待模型!


2. 在 Flutter 中使用快取的 UI

在客戶端,目標很簡單:從 Firestore 擷取預先生成的 A2UI 訊息並將它們顯示在螢幕上。

Commis app displaying pre-generated catering job cards and map navigation

我在代理程式螢幕的初始化期間將其連接起來:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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);
}

如果此函式找到快取的 A2UI 訊息,它會將它們發送到兩個方向:

  • 首先,它將它們提供給我的代理類別 AgentService。它們被加入到系統指令中(「這些 A2UI 訊息已經在客戶端中…」),以便代理知道 UI 的初始狀態。
  • 其次,它透過 addChunk 將它們饋送到 GenUI 傳輸層(仍然是我在整個套件中最喜歡的方法名稱!)。這確保它們被處理,創建正確的表面,並且使用者看到生成的 UI。

完成這兩項任務後,使用者可以立即在螢幕上看到即將到來的餐飲工作卡和地圖 UI——無需等待 LLM 思考。此外,使用者發送給代理程式的後續聊天訊息會在與螢幕上內容的完整歷史記錄的對話中進行。如果使用者看到工作導航卡,他們可以說「實際上,將那項工作的日期更改為下週一」,代理程式就會知道他們正在查看哪個工作並編輯正確的工作。

泛化快取模式

雖然我的特定專案依賴於 Firebase 和 Flutter,但這種預生成和快取模式可以適應您喜歡的任何後端堆疊:

  • 觸發器和工作:直接從資料庫寫入事件生成 UI,或運行定期 cron 作業以預先烘焙即將發生的事件的 UI。
  • 彈性儲存:您不需要笨重的資料庫。快速的鍵值儲存、簡單的快取表,甚至託管在 CDN 上的靜態 JSON 文件都完美適用。
  • 客戶端無關:任何能夠處理 A2UI 的客戶端應用程式(包括使用其他框架建構的應用程式!)都可以讀取這些 JSON 負載並在啟動時渲染它們。

我在這篇文章中使用了相當簡單的範例。它沒有解決多使用者問題或事件結束後的快取失效問題,我想沒有人會對上週末發生的事件的導航幫助感興趣!

話雖如此,我希望這能為您激發一些想法,讓您了解可以使用離線生成的 A2UI 建構哪些體驗,以及如何建立、組合和使用 A2UI 訊息,以充分利用代理應用程式。


試試看!

準備好建構您的第一個 Flutter + A2UI 應用程式或嘗試 GenUI 的非同步模式了嗎?

  • 查看官方的 Intro to GenUI Codelab 以了解生成式 UI 的基礎知識。
  • 探索 GitHub 上的 flutter/demos 儲存庫,以查看 Commis 廚師助理的完整原始碼和其他 Dart 驅動的 GenUI 模式範例。

祝您建構愉快!

來自 Flutter 的更多內容

Flutter’s multiplatform value for agentic development

Flutter 的代理開發多平台價值

Flutter 多平台開發的基本價值在於使用單一共享程式碼庫建構支援多平台的應用程式。

Michael Thomsen
Michael Thomsen
2026 年 5 月 18 日 · 3 分鐘閱讀
We rebuilt Flutter’s websites with Dart and Jaspr

我們使用 Dart 和 Jaspr 重建了 Flutter 的網站

深入了解 Flutter 團隊為何以及如何使用 Dart 和 Jaspr 重建他們的網路形象。

Parker Lougheed
Parker Lougheed
2026 年 4 月 15 日 · 8 分鐘閱讀