A2UI 正在从以 Web 为中心的组件过渡到原生 Android。适用于 A2UI v0.9 的全新 Jetpack Compose 渲染程序使开发者能够以原生方式渲染由 LLM 生成的动态界面。它不使用 WebView,而是利用协程驱动的 actor 框架、密封类生命周期和原生 Material 3 集成。
本讲座将详细介绍新架构,阐释服务器驱动的组件树如何转换为响应式 Compose 节点,以及内置布局过渡如何顺畅处理结构更新,而不会阻塞界面。
转写文稿
大家好,欢迎观看本演讲。今天,我们将探讨如何使用全新的 A2UI 库在 Android 应用中生成界面。
我是 Nick Butcher,负责 Jetpack Compose 的产品经理。
大家好,我是 Anton,Compose 团队的一名工程师。
我正在开发新的 Jetpack Compose A2UI 渲染程序,今天很高兴能向大家介绍它。
是的,现在随着 AI 功能的兴起,许多应用都在研究 AI 如何帮助他们为用户改进或打造全新体验。
一些人正在探索的令人兴奋的体验之一是生成式界面,即在运行时,智能体后端如何为用户创建全新的界面,我们认为这非常有趣。
但遗憾的是,目前许多智能体后端的回答都困在这样的文字墙中。
举个例子,我让一个智能体为我生成一个用于预订行程的新功能。
提示如下。
如您所见,它给出的回答其实相当不错,但却被困在这一大段文字中,我无法轻松与之互动,而且它与我应用的其他部分看起来完全不同。
而生成式界面则让您能够打造这样的体验:您可以根据动态输入即时生成新界面,从而实现更多自定义功能或更多新体验。
下面是一个使用标准原生组件创建的界面示例。
因此,您现在完全可以自行构建这种体验,但您必须定义自己的格式,以便与后端、您自己的事件流和函数等进行交换。
A2UI(即 Agent-to-UI)是一种开放协议,采用 JSON 格式,用于定义所有这些内容。
我非常喜欢 A2UI 的一点是,它是一种声明式格式。
智能体只是返回要呈现的界面的 JSON 表示法。
它不会返回客户端随后需要执行的任何代码,这可能会使您面临某些安全漏洞。
它只是声明式的。
那么,关于 A2UI,我们还需要了解哪些概念?
只有两个主要概念:目录和平台。
目录包含文本、按钮、行和列等组件,代理可以使用这些组件来构建界面。
目录还包含一些函数,智能体可以使用这些函数来设置文本格式、验证用户输入或打开网址。
目录是一份协定,用于定义应用支持的组件和功能,智能体只能使用这些组件和功能进行构建。
这意味着,正如 Nick 所说,代理绝不会在您的应用中执行任意代码。
它会根据您设计和信任的构建块来构建界面。
另一个重要概念是 Surface。
界面是智能体控制的一个界面部分,例如幻灯片上的行程规划师示例。
智能体使用 A2UI 协议消息(例如 createSurface、updateComponents)以及目录中定义的组件和函数,以 JSON 格式描述界面的界面。
说到目录,目录是 A2UI 中一个非常基本的概念,因为它控制着智能体可以使用哪些组件来构建界面。
A2UI 规范定义了基本目录,这是一组可用于构建大多数界面的通用组件。
Jetpack Compose A2UI 渲染程序使用 Material 3 组件提供基本目录的实现。
大多数目录组件都可以开箱即用。
有三个媒体组件:图片、视频和音频播放器,您需要插入所选的库。
例如,您可以选择使用 Coil 加载图片,使用 Media3 播放视频和音频。
不过,A2UI 和我们新推出的渲染器库的真正强大之处在于,您可以创建完全自定义的组件和目录,并全面控制智能体可用于构建界面的内容。
我们稍后会再讨论目录。
好的,我们来看看具体效果。
请切换到演示设备。
这是我们的演示应用,我们将使用它来构建 A2UI 渲染程序,向您展示其简单易用性。
如屏幕中间的这些小条状标签所示,这是一组可以发送给智能体的提示。
因此,如果我们点击其中一个提示,它会将该提示发送到真实的实时代理后端,所以希望演示顺利。除了提示之外,我们还会附加一些系统指令。
因此,我们向后端介绍 A2UI 协议,以及组件目录(在本例中,就是我们在这里使用的基本目录),后端可以使用这些组件来构建界面,我们还会提供一些如何构建界面的示例。
演示应用中的对话会话实现已经知道如何从智能体的回答中提取原始 JSON A2UI 消息。
因此,在这种情况下,智能体回复了一些文本,后跟 A2UI 协议消息:createSurface、updateComponents。
但目前,我们的应用只显示此原始 JSON,尚未将其呈现为界面。
好的,在深入探讨和实现代码之前,我们先回到幻灯片,花一秒钟时间退一步,向您介绍我们将用于实现此功能的系统设计。
哦,想到了。
是的,所以计划是这样的。
因此,AI 智能体会将回答流式传输到我们的应用中,并由 ChatSession 类接收。
ChatSession 会从智能体的流式响应中提取原始 A2UI JSON 消息,并将其传递给 A2UI 消息处理器。
消息处理器配置了一个解析器,用于将这些原始 JSON 消息转换为语义表示。
然后,处理器会根据目录定义验证消息,并将其映射到一组 Surface 模型。
Surface 模型,这些模型定义了界面的外观以及智能体控制的界面部分。
然后,这些 Surface 模型会使用我们的库提供的 A2UI Surface 可组合项呈现为原生 Compose 界面。
智能体与界面之间的通信是双向的,因此当用户与界面互动时,用户操作和错误会回传给智能体。
总而言之,有三大主要构建块:目录、消息处理器和界面。
我们来编写相应的代码。
好,如果能换机器,我们就建。
好的,第一步是添加依赖项。
在这里,您可以看到我们已经添加了 Material 3 A2UI 依赖项,并且已经同步了项目,因此我们不必坐等 Gradle 完成同步。
这是顶级依赖项,它定义了使用这些 Material 3 组件的基本目录的实现,但其下还有其他更细粒度的依赖项。
因此,如果您不想使用基本目录(我们稍后会说明原因),可以使用这些更精细的目录。
但这会以传递方式引入其余依赖项。
好的。
我们说过,有三个方面。
第一步是定义目录。
因此,在我们的应用中,我们将创建一个工厂函数,以帮助构建该 Material 3 实现的基本目录实例。
因此,我们在这里使用这个库函数,即 Material A2UI 基本目录,它也是有版本的。
正如我们所说,该库提供了您所需的所有常见组件,但我们不想强迫您使用特定的图片加载或媒体渲染库,因此您必须自行提供这些库。
例如,Anton 已经创建了这个封装容器,它封装了 Coil 的 AsyncImage 可组合项,我们将使用它来加载图片,并为我们的演示添加一些额外的逻辑,以便从维基百科中查找图片。
因此,我们可以非常轻松地将其插入到目录组件中。
同样,我们也有类似的渲染、视频和音频实现,我们将使用 Media3 库,通过 ExoPlayer 进行渲染。
因此,我们可以像这样非常简单地将自己的组件插入其中。
目录接下来需要的是一组标准函数。
因此,这些是客户端可以提供的功能。
同样,我们也会随基本目录一起提供此接口的实现,但我们需要为其提供一个上下文,您将使用该上下文执行某些操作,例如在用户点按某个内容时打开一个 URI。
这样,我们就完成了第一步。
我们已创建目录。
接下来,我们需要设置消息处理器。
因此,这才是实际解析响应等内容的部分。
因此,我们将切换到应用的视图模型,首先实例化之前设置的目录,因为我们需要用到它。
然后,我们将创建这个消息解析器、消息处理器对象,它需要访问目录,以便根据我们要使用的组件验证回答。
我们还需要创建另一个组件,即解析器,它实际上是负责在 JSON 消息返回时对其进行解析的对象。
在这里,我们创建了 ChatSession 对象。
正如我之前提到的,ChatSession 已经从智能体的回答中提取了原始 A2UI 消息。
当每条新的 A2UI 消息准备就绪时,它会调用此 onA2uiMessage 回调。
因此,我们只需在每条消息到达时将其传递给处理器。
我们调用 processInput 函数,为其提供一个解析器和一条消息。
因此,processInput 函数会解析每条消息,然后将工作排入队列,以便处理器根据目录验证消息并更新表面模型。
因此,处理器会将当前的一组活跃 Surface 模型公开为 activeSurfaces StateFlow。
ChatSession 包含一些逻辑,可将 Surface 模型添加到要呈现的代理回复的界面状态中。
因此,我们只需将此 activeSurfaces StateFlow 作为参数传递给 ChatSession。
此外,正如我所提到的,processInput 函数会将工作排入处理器的队列中。
但要真正开始处理,我们需要在后台线程上启动一个协程。
因此,我们将为此使用视图模型作用域,并设置默认调度程序。
在该协程中,我们只需调用 processor.collectMessages。
因此,当此协程处于活跃状态时,处理器将处理消息队列中的消息。
因此,我们还需要做一件事:当用户清除聊天记录时,我们还需要清除当前 Surface。
因此,我们调用 clearSurfaces。
好的。
我们已经完成了前两个步骤。
我们创建了目录,创建了用于解析消息的处理器。
接下来,我们需要在界面中实际呈现这些回答。
因此,我们会在解析器解析这些表面时收集它们。
现在,在界面中,我们将在代理回复对象中传递这些内容,并简单地遍历它们。
然后,我们将使用这个 A2UI surface 可组合项,并在遍历每个模型时将模型传递给它,以便在所需位置呈现内容。
这里需要注意的一点是,如果智能体选择显示大量内容,它可能会选择使用列表等形式。
因此,我们需要为此组件设置一个有界高度(即最大高度),以确保其正确呈现。
至此,我们已经实现了一个基本的 A2UI 渲染器。
现在,如果我们返回应用,重新启动它,然后再次输入该提示,智能体应该会使用该工具来呈现真实的界面。
因此,我们只需实时编写代码。
这可不是什么障眼法。
但没错,它使用的是真实图片。
它使用了标签页、对话框、日期选择器、滑块等所有标准的 Material 组件,您可以使用这些组件来构建应用,而不仅仅是显示文本回答。
因为这是一个使用真实 Material 组件的真实应用,所以它会执行真实组件所执行的操作。
它具有原生性能。
它会执行一些操作,例如在此处响应深色模式。
这些是 Material 组件。
因此,如果您有一个外部 Material 主题设置可组合项,并且您已在其中自定义了颜色、形状、排版等,那么这些组件将采用这些自定义设置,使其看起来与应用的其余部分一致。
正如 Nick 所说,由于这些是原生 Compose 组件,因此它们具有原生语义。
这意味着 Compose 的所有内置无障碍功能都受支持。
因此,TalkBack、字体缩放、键盘导航等功能开箱即用。
现在,由于您在运行时调用智能体来生成新界面,因此可能会出现一些延迟。
您可能需要等待代理回复。
该库会尽其所能最大限度地缩短此延迟时间。
因此,如果我们使用这种方法,我们会注入一些等待时间,以便更慢地向您展示如何最大限度地减少这种情况。
是的,由于智能体可能需要一些时间才能流式传输完整的回答,因此我们使用了渐进式渲染。
因此,一旦根组件到达,我们就会开始显示该界面的界面。
如果子组件尚未到达,所有容器组件都会为其子组件显示加载占位符。
当子组件到达时,它们会使用 Material 3 运动方案进行动画处理。
好的。
接下来,我们切换到下一个提示,展示该库的另一个功能。
下面是另一个提示,我们正在预订晚餐餐桌。
在这里,智能体选择使用这个滑块组件,是的,它已经使用了。
在这里,你可以看到,你可以拖动并与智能体实时创建的界面进行互动。
请注意,它不仅会更改组件上的数字,还会在下方进一步统计该数字的格式字符串。
比如,告诉我,这不会为了更新而往返于代理,对吧?
这是一种双向绑定。
因此,滑块组件会绑定到此界面的内存中数据模型。
当值发生变化时,绑定到数据模型中同一路径的所有组件都会自动更新。
因此,滑块会读取要显示的值,而且当我拖动滑块时,它会将值写回模型以进行更新。
因此,所有这些操作都在本地完成,无需与代理进行往返通信。
但我们也可以要求代理更新数据。
所以我们假设有 6 个人。
在代理回复时,请注意滑块。
它刚刚跳到了 6。
我们来看看从智能体那里收到了什么。
智能体没有发送全新的界面说明,而是仅发送了一条简短的消息 updateDataModel,用于更新滑块的路径。
由于滑块已绑定到该路径,因此它会就地更新。
因此,每个界面的数据模型都基于 Compose 快照状态构建,并且绑定到路径的每个组件属性都是派生状态。
因此,每当用户或代理更新数据模型中的值时,绑定到同一值的所有组件都会自动重组。
好的。
假设我们想采取行动,点击预订按钮,接下来会发生什么,Anton?
目前不会发生任何变化。
该按钮会分派操作,但我们尚未将其发送给代理。
我们来解决这个问题。
好的,就像我们必须接入处理器来处理输入一样,我们也需要接入任何输出,即用户在代理创建的界面上执行的任何操作。
为此,我们使用处理器公开的 outboundEvents 流,并对此进行收集。
因此,您可以借此机会根据用户点按操作或对界面执行某些操作的时间,插入所需的任何逻辑。
在这里,我们会将所有操作发送回代理,然后由代理决定如何处理,但您也可以在此阶段决定是否调用自己的后端,或导航到应用中的其他界面,这完全取决于您。
Anton 已经将其插入,现在当我们点击“确认预订”操作时,系统会将您执行的操作发送回智能体。
在这里,您甚至可以决定在后端调用某些功能。
我们来看看实际发送给智能体的内容。
这就是 A2UI 操作消息。
它具有一个上下文,其中包含代理要求在执行此操作时发送的一组属性。
因此,当我点击该按钮时,系统会根据数据模型中的当前值,在这一刻解析这些特定属性,并将其发送给代理。
好的,到目前为止,智能体一直非常配合我们,但我们讨论的是 AI,而 AI 可能会出错。
那么,我们来看看如果智能体返回了错误的界面描述,会发生什么情况。
在最后一个提示中,我们实际上是要求智能体犯一个错误,以便向您演示此功能。
我们来看看会发生什么。
没错
如您所见,其中一个组件被标记为有错误。
现在,这个问题已经修复。
这就是智能体自我修正循环。
在每个组件到达界面之前,我们会根据目录架构验证该消息。
如果组件与架构不匹配,则会被标记为错误。
基本目录实现的渲染器显示,该错误仅限于该特定组件,而界面的其余部分仍会显示。
在后台,渲染程序会向智能体发送一条错误消息,让智能体知道出了什么问题。
因此,在这种情况下,我们发送了一条消息,指出在目录中找不到不存在的未知组件类型。
然后,智能体有机会做出回应,发送后续回答来更新现有界面,并且界面在原位置得到了修复。
没错
在 Material 基本目录的当前实现中,我们会立即显示错误,但在自定义组件中,如果出现错误,您可以决定继续显示组件的加载指示器,同时等待代理修复错误。
这样,错误可能完全不会向用户显示。
说到目录和自定义组件,到目前为止,我们只是使用了开箱即用的基本目录,但您当然可以根据自己的需求对其进行自定义。
因此,您可以看到,当我们使用工厂函数创建目录时,所有不同组件都会使用默认值。
因此,您可以根据需要轻松插入自己的组件。
例如,如果我们的应用有自己的设计体系和按钮,我们就可以轻松地将其插入。
因此,我们只需在创建目录实例时替换此参数,然后创建此组件定义。
因此,如果我们创建一个新类来实现该接口,您可以看到这里有一个接口,即 A2UI 基本目录,其类型为按钮。
因此,如果我们实现此接口,就可以插入自己的组件。
没错
由于我们要替换基本目录中的组件,因此需要实现此 A2UI 基本目录按钮接口。
因此,此界面已经包含有关组件的一些信息,例如名称、说明和属性架构。
因此,要实现我们自己的版本,只需实现此可组合函数即可。
它需要几个参数。
因此,这些参数是经过验证后从代理接收的值。
这些内容包括子 ID,它引用了将作为按钮内容的组件。
它还有一个变体,用于描述按钮类型、操作定义和一些其他参数。
在按钮的生产实现中,我们需要使用所有这些参数,但出于演示目的,我们只需要 child 和 action。
我们来实现这个可组合项。
在这个可组合函数中,我们将发出自定义的“顶”按钮类型。
如您所见,我们可以使用 A2UI 提供并验证的参数。
例如,在 onClick 函数中,我们可以分派该操作。
我们可以使用此库类型 DispatchAction,并向其传递 A2UI 定义的操作。
因此,我们使用的是自己的按钮,但仍希望呈现子内容、标签或代理决定使用的任何内容。
为此,我们使用 observeA2uiComponentState 函数并向其传递 childId,以便获取应呈现的子级内容的句柄。
然后,我们将使用这个 A2uiComponent 可组合函数,并向其传递该子组件状态以进行渲染。
在这里,您可以看到,我们仅在子内容成功加载时才呈现该内容,但您也可以在这里添加自己的逻辑,以便在加载或出现错误状态时,决定要显示的内容。
因此,如果我们回到演示应用中的一个示例提示,现在使用标准 Material 按钮替换该按钮。
非常细微,希望您能看到。
您可以看到,我们在这里也使用了非常个性化的按钮。
太棒了,这就是如何替换一些库类型的方法。但如果您想使用自己的自定义类型来扩展库,该怎么办?
我们可以通过向目录本身添加内容来轻松实现这一点。
Anton 将在此处创建一个新类,以实现星级评分组件。
因此,我们将向应用中添加一些内容,以便在这次假设的行程中对餐馆进行评分。
因此,由于我们不是要替换现有组件,所以我们使用这个更通用的接口,即 A2UI 组件接口。如您所见,该接口有一些我们需要替换的形参。
前两个是名称和说明。
因此,这描述了我们的组件是什么以及它的作用。
这些非常重要,因为当我们将目录序列化并发送到后端,告诉它可以使用什么来构建界面时,智能体将使用这些信息来决定在响应时是否使用此组件。
因此,您需要提供非常详细的描述,并告诉 AI 您确切知道此操作的作用。
我们需要替换的下一个属性是 properties 属性,它基本上定义了此组件处理的数据。
因此,我们要创建一个星级评分栏,以便您对餐厅进行评分,我们需要两个属性。
它将是值(例如用户选择了多少颗星,这将是用户可以与之互动并更改的可变属性)和最大星数。
在这里,我们可以看到 Anton 使用了这个动态数字类型属性来表示值。
因此,这就像您知道实际选择了多少颗星。
该参数也有自己的描述,用于告知智能体此值的用途。
然后,我们有一个静态类型属性,因此我们只需使用这个 A2UI 属性(类型为数字),来定义此组件可以显示的最大星级数。
定义好这两个属性后,我们只需返回它们。
因此,在替换此 properties 属性时,我们将创建一个列表并返回这些属性。
这就是您告诉智能体此组件处理的内容的方式。
没错
正如 Nick 所说,这些属性定义会作为目录架构的一部分发送,以便智能体了解如何使用此组件。
但这些属性定义也用于在运行时提取类型化值。
因此,只有一个单一事实来源。
那么,我们来看看需要做些什么来实现这一点。
首先,假设我们要访问 max 属性。由于 max 属性是静态属性,这意味着代理会在 properties 对象中以内嵌方式发送其值。
因此,如需访问它,我们只需使用 proper-max 属性对象作为键,从 properties 对象中引用它即可。
由于此属性被定义为数字,因此我们还需要将其转换为 int 并设置默认值。
接下来是 value 属性。
由于 value 属性是动态属性,因此它会引用数据模型中的内容。
因此,如需访问它,我们需要使用 properties.bind API,并将属性对象作为键作为参数传递。
因此,此操作会返回数据模型中存储的此属性的当前值,并订阅未来的更新。
因此,每当数据模型更新时,此组件都会自动重组。
同样,我们还将其转换为 int 并设置了一些默认值。
我们还需要做一件事,那就是设置双向绑定,以便更改评分。
为此,我们使用 bindUpdater API。
我们传递定义属性的同一属性对象,因此,bindUpdater 会返回一个 lambda,使我们能够更改数据模型中此属性的值。
现在,我们可以发出星级组件了。
好的。
现在,我们已经定义了自己的自定义组件,接下来需要将其添加到我们正在使用的目录中。
之前,我们只是直接使用基本目录。
现在,我们需要创建一个新目录,该目录将包含所有基本目录并添加我们的自定义组件。
因此,目录由三部分组成。
它是一个 ID、一组组件和一组函数。
对于 ID,我们将使用 URI 来定义,也就是唯一标识目录以及我们正在使用的版本。
然后,对于组件,我们将从基本目录中获取所有组件,再使用 add 运算符添加我们刚刚创建的自定义组件。
对于函数,我们将使用基本目录中的函数。
这样,我们就扩展了基本目录,添加了新的自定义组件类型。
看,创建自定义目录非常简单。
因此,智能体会通过我们发送给它的系统指令来了解如何使用目录。
现在,我们已经创建了自定义目录,接下来还要更新系统指令。
因此,我们将把 catalog 对象传递给构建系统指令的函数。
所以,就是这样。
因此,到目前为止,由于我们使用的是标准目录,所以我们发送的是基本目录的静态架构定义。
现在我们有了自定义目录,可以使用 toJsonSchemaString 函数动态生成架构。
因此,这将创建此目录的完整 JSON 表示法,包括我们刚刚添加的星级组件。
系统指令还包含一些智能体的回答示例。
因此,我们还要在此处更改目录 ID,以便示例引用正确的目录。
好的。
这样,我们就扩展了基本目录,添加了我们自己的自定义类型。
因此,如果我们重新部署该应用并尝试其中一个提示,应该会看到评分界面。
然后,智能体给出回答。
在这里,您可以看到这个星级组件是一个全新的组件,在刚刚实现之前,智能体从未见过它。
这不是基本目录的一部分,而是我们刚刚创建的。
当我点击星标时,系统会调用 bindUpdater lambda,并在数据模型中更新该值。
如果我们提交评分,就可以查看操作,操作的上下文将包含我们刚刚设置的值。
好的,现在我们切换回幻灯片。
我们刚才快速讲解了很多内容,但希望我们已经展示了如何轻松地从智能体以大段文字回复,转变为它们实际创建真正的原生界面。
希望我们已经向您展示了集成非常简单。
您需要完成以下三个主要部分:创建组件目录、实现一些解析逻辑,然后在界面中呈现。
希望我们已经展示了该库开箱即用的强大功能,例如渐进式渲染、架构验证、自我修正循环和错误处理等,它能做的事情非常多。
今天,我们快速演示了如何轻松自定义该模型。
我们展示了如果您使用的是 Material,基本目录如何响应您的 Material 主题设置,但扩展或替换目录中的组件其实非常简单。
但我认为,最强大的功能之一是,您可以创建自己的目录。
如果您希望最大程度地控制代理可以实际使用的界面,则可以创建完全自定义的组件。
如果您想让它完全自由发挥,可以给它提供非常基本的组件,例如文本标签和按钮,然后说:“你来创建界面。”
如果您想获得更多控制权,可以创建更复杂的组件,并指定“这些是您唯一可以使用的组件”。
因此,您可以控制代理可以执行的操作以及向用户显示的内容。
这就是我们刚刚推出的新 A2UI 渲染程序库和 Jetpack 库。
implementation("androidx.compose.material3:material3-a2ui:1.0.0-alpha02")它目前处于 Alpha 版阶段,Alpha 02 版刚刚发布。
请点击这些链接查看。
我们在 developer.android.com 上提供了相关文档以及一些示例。
我们有一个展示基本目录的基本示例,还有一个更复杂的示例,以及我们刚刚创建的这个 JetPacker 示例应用,您将在今天晚些时候的 ADS 主题演讲中再次看到它,它演示了如何创建这些更复杂的组件来控制用户看到的内容。
正如我所说,我们目前处于 Alpha 阶段,因此现在是试用该工具并向我们提供反馈和功能请求的绝佳时机,您可以告诉我们您希望如何使用该工具,或者如果该工具可以执行 X 或 Y 操作,您会很乐意使用它。
因此,请务必查看该库并向我们提供反馈。
这就是我们得到的结果。
我迫不及待地想看到您和您的智能体用它构建出什么。
非常感谢。
谢谢。