A2UI Android Compose Renderer

February 15, 2026 · View on GitHub

License: Apache 2.0 Kotlin API

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

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

FeatureDescriptionStatus
A2UI v0.10 ProtocolFull support for createSurface, updateComponents, updateDataModel, deleteSurface
Dynamic Component Rendering20+ standard components, custom component registration support
Data BindingOne-way/two-way data binding, path expressions
Input Validationrequired, email, url, phone, regex, etc.
Theme CustomizationDynamic colors, dark mode, custom themes
Network TransportWebSocket, SSE (Server-Sent Events)
State PersistenceAutomatic save/restore on configuration changes
Error HandlingGlobal error handler, error display components
AccessibilityTalkBack support, semantic labels, touch targets
AnimationsModal animations, transition animations

📦 Supported Components

ComponentDescriptionAccessibility
TextText display with h1/h2/h3/title/subtitle/body/caption/label variants
ButtonButton with primary/secondary/text variants
TextFieldText input with validation rules
CheckBoxCheckbox
SwitchSwitch toggle
SliderSlider
ChoicePickerSingle/multi select picker
DropdownDropdown select
CardCard container
RowHorizontal layout container
ColumnVertical layout container
ListScrollable list
TabsTabs
ModalModal dialog (with animations)
ImageImage display (Coil loading)
IconIcon display
DividerDivider line
SpacerSpacer
ProgressBarProgress bar
DateTimeInputDate time picker
VideoVideo player (placeholder)
AudioPlayerAudio player (placeholder)
SurfaceBase 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

  1. Clone repository

    git clone https://github.com/lmee/A2UI-Android.git
    cd A2UI-Android
    
  2. 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")
    
  3. 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:

RuleDescriptionParameters
requiredRequired field validation-
emailEmail format validation-
urlURL format validation-
phonePhone number validation-
minLengthMinimum length validationmin: Int
maxLengthMaximum length validationmax: Int
regexRegular expression validationpattern: String
numericNumeric validationmin: 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"
    }
}
{
    "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 TypeDescription
ParseErrorJSON parsing error
NetworkErrorNetwork connection error
ComponentErrorComponent rendering error
ValidationErrorInput validation error
StateErrorState management error
UnknownErrorUnknown 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

  1. State Persistence: Use rememberSaveable to save state
  2. List Optimization: LazyColumn uses key parameter
  3. Conditional Updates: LaunchedEffect conditional checks to avoid unnecessary updates
  4. 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

  1. Video Component: Currently a placeholder implementation, needs ExoPlayer integration
  2. AudioPlayer Component: Currently a placeholder implementation, needs MediaPlayer integration
  3. 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

  1. Fork and clone the repository
  2. Open the project in Android Studio
  3. Run ./gradlew :android_compose:build to 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.