A2UI Android Compose Renderer
February 15, 2026 · View on GitHub
English | 中文
Based on A2UI Protocol - A full-featured Android Jetpack Compose implementation of the A2UI (Agent to UI) protocol renderer, enabling Android applications to dynamically render user interfaces generated by A2UI backend agents.
📖 Table of Contents
- Overview
- Features
- Architecture
- Quick Start
- Installation
- Core Features
- Components
- Examples
- Theming
- Error Handling
- Network Transport
- Accessibility
- Performance
- Testing
- API Reference
- Notes
- Contributing
- License
Overview
A2UI Android Compose Renderer is a complete implementation of the A2UI protocol for the Android platform, built with modern Jetpack Compose technology stack. It supports all core features of the A2UI v0.10 protocol, including dynamic component rendering, data binding, theme customization, network transport, and more.
Why Choose A2UI Compose?
- Declarative UI: Built on Jetpack Compose with modern declarative UI paradigm
- Reactive Updates: Built-in state management with efficient data binding and UI updates
- Highly Customizable: Support for custom components, themes, validation rules, etc.
- Full Compatibility: Supports Android 5.0+ (API 21+), covering 99%+ of Android devices
- Performance Optimized: Uses
rememberSaveable,key()and other techniques to optimize recomposition - Accessibility: Built-in WCAG A level accessibility support
Features
✅ Core Features
| Feature | Description | Status |
|---|---|---|
| A2UI v0.10 Protocol | Full support for createSurface, updateComponents, updateDataModel, deleteSurface | ✅ |
| Dynamic Component Rendering | 20+ standard components, custom component registration support | ✅ |
| Data Binding | One-way/two-way data binding, path expressions | ✅ |
| Input Validation | required, email, url, phone, regex, etc. | ✅ |
| Theme Customization | Dynamic colors, dark mode, custom themes | ✅ |
| Network Transport | WebSocket, SSE (Server-Sent Events) | ✅ |
| State Persistence | Automatic save/restore on configuration changes | ✅ |
| Error Handling | Global error handler, error display components | ✅ |
| Accessibility | TalkBack support, semantic labels, touch targets | ✅ |
| Animations | Modal animations, transition animations | ✅ |
📦 Supported Components
| Component | Description | Accessibility |
|---|---|---|
| Text | Text display with h1/h2/h3/title/subtitle/body/caption/label variants | ✅ |
| Button | Button with primary/secondary/text variants | ✅ |
| TextField | Text input with validation rules | ✅ |
| CheckBox | Checkbox | ✅ |
| Switch | Switch toggle | ✅ |
| Slider | Slider | ✅ |
| ChoicePicker | Single/multi select picker | ✅ |
| Dropdown | Dropdown select | ✅ |
| Card | Card container | ✅ |
| Row | Horizontal layout container | ✅ |
| Column | Vertical layout container | ✅ |
| List | Scrollable list | ✅ |
| Tabs | Tabs | ✅ |
| Modal | Modal dialog (with animations) | ✅ |
| Image | Image display (Coil loading) | ✅ |
| Icon | Icon display | ✅ |
| Divider | Divider line | ✅ |
| Spacer | Spacer | ✅ |
| ProgressBar | Progress bar | ✅ |
| DateTimeInput | Date time picker | ✅ |
| Video | Video player (placeholder) | ✅ |
| AudioPlayer | Audio player (placeholder) | ✅ |
| Surface | Base container | ✅ |
Architecture
Project Structure
android_compose/
├── src/
│ ├── main/
│ │ ├── java/org/a2ui/compose/
│ │ │ ├── data/ # Data Layer
│ │ │ │ ├── A2UIMessage.kt # Message type definitions
│ │ │ │ ├── DataModelProcessor.kt # Data model processor
│ │ │ │ └── DataModelState.kt # Data model state
│ │ │ ├── rendering/ # Rendering Layer
│ │ │ │ ├── A2UIRenderer.kt # Main renderer
│ │ │ │ └── ComponentRegistry.kt # Component registry
│ │ │ ├── transport/ # Network Transport Layer
│ │ │ │ ├── A2UITransport.kt # Transport interface
│ │ │ │ └── NetworkTransport.kt # WebSocket/SSE implementation
│ │ │ ├── theme/ # Theme Layer
│ │ │ │ └── A2UITheme.kt # Theme configuration
│ │ │ ├── error/ # Error Handling Layer
│ │ │ │ └── ErrorHandler.kt # Error handler
│ │ │ ├── service/ # Service Layer
│ │ │ │ └── A2UIService.kt # High-level service API
│ │ │ └── example/ # Example Code
│ │ │ ├── A2UIDemoActivity.kt # Demo app
│ │ │ └── A2UISampleActivity.kt # Sample activity
│ │ ├── res/ # Resources
│ │ │ └── values/
│ │ │ ├── colors.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ └── AndroidManifest.xml
│ └── test/ # Unit Tests
│ └── java/org/a2ui/compose/
│ ├── data/
│ │ ├── DataModelStateTest.kt
│ │ └── DataModelProcessorTest.kt
│ ├── rendering/
│ │ └── A2UIRendererTest.kt
│ └── theme/
│ └── A2UIThemeTest.kt
├── build.gradle.kts # Build configuration
├── README.md # Chinese documentation
└── README_EN.md # English documentation
Core Architecture Diagram
┌─────────────────────────────────────────────────────────────┐
│ A2UI Agent (Backend) │
└─────────────────────────┬───────────────────────────────────┘
│ A2UI Messages (JSON)
▼
┌─────────────────────────────────────────────────────────────┐
│ Transport Layer │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ WebSocket │ │ SSE │ │
│ │ Transport │ │ Transport │ │
│ └────────┬────────┘ └────────┬────────┘ │
└───────────┼─────────────────────┼───────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ A2UI Renderer │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Message Processor │ │
│ │ • CreateSurface • UpdateComponents │ │
│ │ • UpdateDataModel • DeleteSurface │ │
│ └───────────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼─────────────────────────────┐ │
│ │ Data Model Processor │ │
│ │ • State Management • Data Binding │ │
│ │ • Validation • Dynamic Value Resolution │ │
│ └───────────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼─────────────────────────────┐ │
│ │ Component Registry │ │
│ │ • Standard Components • Custom Components │ │
│ │ • Accessibility • Animations │ │
│ └───────────────────────┬─────────────────────────────┘ │
└──────────────────────────┼──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Jetpack Compose UI │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Text │ │ Button │ │ TextField│ │ Card │ ... │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────────┘
Quick Start
Requirements
- Android Studio Hedgehog (2023.1.1) or later
- Android SDK 21+ (Android 5.0 Lollipop)
- Kotlin 1.9.22
- JDK 17
5-Minute Integration
// 1. Create renderer in Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val renderer = rememberA2UIRenderer()
// Process A2UI message
renderer.processMessage("""
{
"version": "v0.10",
"createSurface": {
"surfaceId": "hello",
"catalogId": "https://a2ui.org/catalog.json"
}
}
""")
renderer.processMessage("""
{
"version": "v0.10",
"updateComponents": {
"surfaceId": "hello",
"components": [
{
"id": "root",
"component": "Text",
"text": "Hello, A2UI!"
}
]
}
}
""")
// Render UI
A2UISurface(surfaceId = "hello")
}
}
}
Installation
Option 1: Module Integration
-
Clone repository
git clone https://github.com/lmee/A2UI-Android.git cd A2UI-Android -
Add module to project
In your project's
settings.gradle.kts:include(":android_compose") project(":android_compose").projectDir = file("path/to/A2UI-Android/android_compose") -
Add dependency
In your app module's
build.gradle.kts:dependencies { implementation(project(":android_compose")) }
Option 2: Copy Source Code
Directly copy the android_compose/src/main/java/org/a2ui/compose directory to your project.
Dependencies
The project depends on the following libraries (configured in build.gradle.kts):
// Jetpack Compose
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
// Kotlin
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// Image Loading
implementation("io.coil-kt:coil-compose:2.5.0")
// Network
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:okhttp-sse:4.12.0")
Core Features
1. Message Processing
The A2UI renderer updates the UI by processing JSON messages:
val renderer = A2UIRenderer()
// Create Surface
renderer.processMessage("""
{
"version": "v0.10",
"createSurface": {
"surfaceId": "my_surface",
"catalogId": "https://a2ui.org/catalog.json",
"theme": { "primaryColor": "#6200EE" }
}
}
""")
// Update Components
renderer.processMessage("""
{
"version": "v0.10",
"updateComponents": {
"surfaceId": "my_surface",
"components": [ /* component definitions */ ]
}
}
""")
// Update Data Model
renderer.processMessage("""
{
"version": "v0.10",
"updateDataModel": {
"surfaceId": "my_surface",
"path": "/user/name",
"value": "John Doe"
}
}
""")
// Delete Surface
renderer.processMessage("""
{
"version": "v0.10",
"deleteSurface": {
"surfaceId": "my_surface"
}
}
""")
2. Data Binding
Support for path expressions in data binding:
// Use path binding in component definition
{
"id": "name_field",
"component": "TextField",
"label": "Name",
"value": { "path": "/user/name" },
"placeholder": "Enter your name"
}
// Support nested paths
{
"id": "city_field",
"component": "Text",
"text": { "path": "/user/address/city" }
}
3. Input Validation
Built-in validation rules:
{
"id": "email_field",
"component": "TextField",
"label": "Email",
"value": { "path": "/form/email" },
"required": true,
"checks": [
{
"call": "email",
"args": {},
"message": "Please enter a valid email"
}
]
}
Supported Validation Rules:
| Rule | Description | Parameters |
|---|---|---|
required | Required field validation | - |
email | Email format validation | - |
url | URL format validation | - |
phone | Phone number validation | - |
minLength | Minimum length validation | min: Int |
maxLength | Maximum length validation | max: Int |
regex | Regular expression validation | pattern: String |
numeric | Numeric validation | min: Number, max: Number |
4. Action Handling
Handle user interaction events:
val renderer = A2UIRenderer()
renderer.setActionHandler(object : ActionHandler {
override fun onAction(surfaceId: String, actionName: String, context: Map<String, Any>) {
when (actionName) {
"submit_form" -> {
val formData = renderer.getDataModel(surfaceId)?.getDataSnapshot()
// Handle form submission
}
}
}
override fun openUrl(url: String) {
// Open URL
}
override fun showToast(message: String) {
// Show Toast
}
})
Components
Text Component
{
"id": "title",
"component": "Text",
"text": "Hello World",
"variant": "h2"
}
variant options: h1, h2, h3, title, subtitle, body, caption, label
Button Component
{
"id": "submit_btn",
"component": "Button",
"text": "Submit",
"variant": "primary",
"action": {
"event": {
"name": "submit",
"context": { "formId": "contact" }
}
}
}
variant options: primary, secondary, text
TextField Component
{
"id": "email",
"component": "TextField",
"label": "Email Address",
"value": { "path": "/form/email" },
"placeholder": "Enter your email",
"required": true,
"checks": [
{ "call": "email", "args": {}, "message": "Invalid email format" }
]
}
List Component
{
"id": "item_list",
"component": "List",
"children": {
"path": "/items",
"componentId": "list_item"
}
}
Modal Component
{
"id": "confirm_dialog",
"component": "Modal",
"child": "dialog_content",
"action": {
"event": { "name": "dismiss" }
}
}
Examples
Complete Form Example
@Composable
fun ContactFormScreen() {
val renderer = rememberA2UIRenderer()
DisposableEffect(Unit) {
// Create Surface
renderer.processMessage("""
{
"version": "v0.10",
"createSurface": {
"surfaceId": "contact_form",
"catalogId": "https://a2ui.org/catalog.json",
"theme": { "primaryColor": "#6200EE" }
}
}
""")
// Define components
renderer.processMessage("""
{
"version": "v0.10",
"updateComponents": {
"surfaceId": "contact_form",
"components": [
{"id": "root", "component": "Card", "child": "form"},
{"id": "form", "component": "Column", "children": ["title", "name", "email", "message", "submit"], "align": "stretch"},
{"id": "title", "component": "Text", "text": "Contact Us", "variant": "h2"},
{"id": "name", "component": "TextField", "label": "Name", "value": {"path": "/name"}, "required": true},
{"id": "email", "component": "TextField", "label": "Email", "value": {"path": "/email"}, "required": true, "checks": [{"call": "email", "args": {}, "message": "Invalid email"}]},
{"id": "message", "component": "TextField", "label": "Message", "value": {"path": "/message"}, "variant": "longText"},
{"id": "submit", "component": "Button", "text": "Send", "action": {"event": {"name": "submit_contact"}}}
]
}
}
""")
onDispose {
renderer.processMessage("""{"version": "v0.10", "deleteSurface": {"surfaceId": "contact_form"}}""")
}
}
RenderSurface(renderer, "contact_form")
}
Theming
Using Theme Configuration
@Composable
fun ThemedApp() {
val themeConfig = A2UIThemeConfig(
primaryColor = "#6200EE",
secondaryColor = "#03DAC6",
backgroundColor = "#FFFFFF",
surfaceColor = "#FFFFFF",
errorColor = "#B00020",
darkMode = false,
borderRadius = 12,
fontFamily = "Roboto"
)
A2UITheme(config = themeConfig) {
// Your A2UI interface
A2UISurface(surfaceId = "main")
}
}
Dynamic Theme Switching
@Composable
fun DynamicThemeApp() {
var isDarkMode by remember { mutableStateOf(false) }
val themeConfig = A2UIThemeConfig(
primaryColor = if (isDarkMode) "#BB86FC" else "#6200EE",
darkMode = isDarkMode
)
A2UITheme(config = themeConfig) {
Column {
Switch(
checked = isDarkMode,
onCheckedChange = { isDarkMode = it }
)
A2UISurface(surfaceId = "main")
}
}
}
Error Handling
Global Error Handler
val errorHandler = DefaultErrorHandler()
val renderer = A2UIRenderer(
logger = DefaultLogger(),
errorHandler = errorHandler
)
// Display errors
@Composable
fun ErrorAwareScreen() {
val errors by remember { derivedStateOf { errorHandler.errors } }
Column {
// Display error banners
errors.forEachIndexed { index, errorInfo ->
ErrorBanner(
errorInfo = errorInfo,
onDismiss = { errorHandler.dismissError(index) },
onRetry = errorInfo.recoveryAction
)
}
// Main interface
A2UISurface(surfaceId = "main")
}
}
Error Types
| Error Type | Description |
|---|---|
ParseError | JSON parsing error |
NetworkError | Network connection error |
ComponentError | Component rendering error |
ValidationError | Input validation error |
StateError | State management error |
UnknownError | Unknown error |
Network Transport
WebSocket Connection
val transport = WebSocketTransport(
url = "wss://your-server.com/a2ui",
reconnectEnabled = true,
reconnectDelayMs = 3000
)
// Connect
scope.launch {
transport.connect()
transport.messages.collect { message ->
renderer.processMessage(message)
}
}
// Send message
transport.send("""{"action": "ping"}""")
SSE Connection
val transport = SSETransport(
url = "https://your-server.com/a2ui/stream",
reconnectEnabled = true
)
scope.launch {
transport.connect()
transport.messages.collect { message ->
renderer.processMessage(message)
}
}
Accessibility
WCAG A Level Compliance
The renderer has built-in accessibility support:
- Semantic Labels: All components have
contentDescription - Role Identification: Button, CheckBox, Switch have correct
Role - State Descriptions: CheckBox, Switch have state descriptions
- Live Regions: Error messages use
LiveRegionMode.Polite - Touch Targets: All clickable elements have minimum 48dp
Custom Accessibility
// Components automatically handle accessibility
// For customization, add to component definition:
{
"id": "custom_button",
"component": "Button",
"text": "Submit",
"accessibilityLabel": "Submit the contact form"
}
Performance
Implemented Optimizations
- State Persistence: Use
rememberSaveableto save state - List Optimization: LazyColumn uses
keyparameter - Conditional Updates:
LaunchedEffectconditional checks to avoid unnecessary updates - Component Reuse: Component reuse through ComponentRegistry
Performance Best Practices
// ✅ Recommended: Use rememberA2UIRenderer
val renderer = rememberA2UIRenderer()
// ✅ Recommended: Use DisposableEffect for cleanup
DisposableEffect(surfaceId) {
// Initialize
onDispose {
// Cleanup
}
}
// ✅ Recommended: Use key to stabilize component identity
key(component.id) {
render(component, context)
}
Testing
Unit Tests
The project includes comprehensive unit tests:
src/test/java/org/a2ui/compose/
├── data/
│ ├── DataModelStateTest.kt # 9 tests
│ └── DataModelProcessorTest.kt # 13 tests
├── rendering/
│ └── A2UIRendererTest.kt # 16 tests
└── theme/
└── A2UIThemeTest.kt # 11 tests
Running Tests
# Run all unit tests
./gradlew :android_compose:test
# Run specific test class
./gradlew :android_compose:test --tests "org.a2ui.compose.rendering.A2UIRendererTest"
API Reference
A2UIRenderer
Main renderer class, responsible for processing messages and managing UI state.
class A2UIRenderer(
logger: A2UILogger = DefaultLogger(),
errorHandler: A2UIErrorHandler? = null
) {
// Process A2UI message
fun processMessage(message: String): Result<Unit>
// Get Surface context
fun getSurfaceContext(surfaceId: String): SurfaceContext?
// Get component
fun getComponent(surfaceId: String, componentId: String): Component?
// Get data model
fun getDataModel(surfaceId: String): DataModelState?
// Set action handler
fun setActionHandler(handler: ActionHandler?)
// Save/restore state
fun saveState(): SavedRendererState
fun restoreState(state: SavedRendererState)
// Cleanup resources
fun dispose()
}
ComponentRegistry
Component registry, manages rendering of all components.
class ComponentRegistry(renderer: A2UIRenderer) {
// Register custom component
fun registerCustomComponent(
name: String,
factory: @Composable (Component, SurfaceContext) -> Unit
)
// Unregister custom component
fun unregisterCustomComponent(name: String)
// Render component
@Composable
fun render(component: Component, context: SurfaceContext)
}
A2UITheme
Theme configuration Composable.
@Composable
fun A2UITheme(
config: A2UIThemeConfig = A2UIThemeConfig(),
darkTheme: Boolean = config.darkMode ?: isSystemInDarkTheme(),
content: @Composable () -> Unit
)
data class A2UIThemeConfig(
val primaryColor: String? = null,
val secondaryColor: String? = null,
val backgroundColor: String? = null,
val surfaceColor: String? = null,
val textColor: String? = null,
val errorColor: String? = null,
val darkMode: Boolean? = null,
val borderRadius: Int = 8,
val fontFamily: String? = null
)
Notes
Compatibility
- Minimum SDK: Android 5.0 (API 21)
- Target SDK: Android 14 (API 34)
- Kotlin Version: 1.9.22
Known Limitations
- Video Component: Currently a placeholder implementation, needs ExoPlayer integration
- AudioPlayer Component: Currently a placeholder implementation, needs MediaPlayer integration
- Markdown Rendering: Not yet implemented
Migration Guide
Migrating from earlier versions:
// Old version
val renderer = A2UIRenderer()
renderer.processMessage(message)
// New version (recommended)
val renderer = rememberA2UIRenderer()
renderer.processMessage(message)
Debugging Tips
// Enable verbose logging
val logger = object : A2UILogger {
override fun log(level: A2UILogLevel, message: String) {
Log.d("A2UI", "[$level] $message")
}
}
val renderer = A2UIRenderer(logger = logger)
Contributing
We welcome all forms of contribution! See CONTRIBUTING.md for details.
Development Setup
- Fork and clone the repository
- Open the project in Android Studio
- Run
./gradlew :android_compose:buildto verify the build
Code Style
- Follow Kotlin official code style
- Use 4-space indentation
- All public APIs must have KDoc comments
License
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.