Skip to content

Commit 69591f4

Browse files
feat(android): Add interactive input and fix UI rendering
1 parent 4c9c367 commit 69591f4

File tree

1 file changed

+77
-51
lines changed
  • samples/client/android/projects/contact/src/main/java/com/google/a2ui/sample

1 file changed

+77
-51
lines changed

samples/client/android/projects/contact/src/main/java/com/google/a2ui/sample/MainActivity.kt

Lines changed: 77 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@ import androidx.activity.ComponentActivity
66
import androidx.activity.compose.setContent
77
import androidx.compose.foundation.layout.Box
88
import androidx.compose.foundation.layout.fillMaxSize
9+
import androidx.compose.foundation.layout.fillMaxWidth
910
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.foundation.layout.width
12+
import androidx.compose.material3.Button
1013
import androidx.compose.material3.CircularProgressIndicator
1114
import androidx.compose.material3.MaterialTheme
1215
import androidx.compose.material3.Scaffold
1316
import androidx.compose.material3.Surface
1417
import androidx.compose.material3.Text
18+
import androidx.compose.material3.TextField
19+
import androidx.compose.ui.unit.dp
1520
import androidx.compose.runtime.*
1621
import androidx.compose.ui.Alignment
1722
import androidx.compose.ui.Modifier
@@ -40,68 +45,89 @@ class MainActivity : ComponentActivity() {
4045
@Composable
4146
fun SampleA2UIScreen() {
4247
val surfaceState = remember { SurfaceState() }
43-
var isLoading by remember { mutableStateOf(true) }
48+
var isLoading by remember { mutableStateOf(false) }
4449
var errorMsg by remember { mutableStateOf<String?>(null) }
50+
var query by remember { mutableStateOf("Find contact info for Alex Jordan") }
4551

4652
// Initialize our native A2A Client
4753
val a2aClient = remember { A2AClient() }
54+
val scope = rememberCoroutineScope()
4855

49-
LaunchedEffect(Unit) {
50-
withContext(Dispatchers.IO) {
51-
try {
52-
// Send an initial query to start the conversation
53-
val messages = a2aClient.sendMessage("Find contact info for Alex Jordan")
54-
55-
withContext(Dispatchers.Main) {
56-
android.util.Log.d("MainActivity", "Applying ${messages.size} updates to surface.")
57-
messages.forEach {
58-
android.util.Log.d("MainActivity", "Applying update: $it")
59-
surfaceState.applyUpdate(it)
56+
androidx.compose.foundation.layout.Column(
57+
modifier = Modifier.fillMaxSize()
58+
) {
59+
// Input Area
60+
androidx.compose.foundation.layout.Row(
61+
modifier = Modifier
62+
.fillMaxWidth()
63+
.padding(16.dp),
64+
verticalAlignment = Alignment.CenterVertically
65+
) {
66+
androidx.compose.material3.TextField(
67+
value = query,
68+
onValueChange = { query = it },
69+
modifier = Modifier.weight(1f),
70+
label = { Text("Ask Agent") }
71+
)
72+
androidx.compose.foundation.layout.Spacer(modifier = Modifier.width(8.dp))
73+
androidx.compose.material3.Button(
74+
onClick = {
75+
isLoading = true
76+
errorMsg = null
77+
scope.launch(Dispatchers.IO) {
78+
try {
79+
val messages = a2aClient.sendMessage(query)
80+
withContext(Dispatchers.Main) {
81+
messages.forEach { surfaceState.applyUpdate(it) }
82+
isLoading = false
83+
}
84+
} catch (e: Exception) {
85+
e.printStackTrace()
86+
withContext(Dispatchers.Main) {
87+
errorMsg = "Error: ${e.message}"
88+
isLoading = false
89+
}
90+
}
6091
}
61-
isLoading = false
62-
}
63-
} catch (e: Exception) {
64-
e.printStackTrace()
65-
withContext(Dispatchers.Main) {
66-
errorMsg = "Error: ${e.message}. \nMake sure 'uv run .' is running in 'contact_lookup'!"
67-
isLoading = false
68-
}
92+
},
93+
enabled = !isLoading
94+
) {
95+
Text("Send")
6996
}
7097
}
71-
}
7298

73-
if (isLoading) {
74-
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
75-
CircularProgressIndicator()
76-
}
77-
} else if (errorMsg != null) {
78-
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
79-
Text(text = errorMsg!!)
80-
}
81-
} else {
82-
A2UISurface(
83-
surfaceId = "contact-card", // Matches server's "contact-card" surface ID
84-
state = surfaceState,
85-
onUserAction = { action, src, contextMap ->
86-
Log.d("A2UI", "Action: ${action.name} from $src with $contextMap")
87-
Log.d("A2UI", "Action: ${action.name} from $src with $contextMap")
88-
89-
// Construct the JSON object for the user action context
90-
val actionData = kotlinx.serialization.json.JsonObject(contextMap)
91-
92-
// Launch a coroutine to send the event to the server
93-
// Note: ideally this should be handled by a ViewModel to avoid leak
94-
kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch {
95-
try {
96-
val updates = a2aClient.sendEvent(actionData)
97-
withContext(Dispatchers.Main) {
98-
updates.forEach { surfaceState.applyUpdate(it) }
99+
// Content Area
100+
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
101+
if (isLoading) {
102+
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
103+
} else if (errorMsg != null) {
104+
Text(
105+
text = errorMsg!!,
106+
color = MaterialTheme.colorScheme.error,
107+
modifier = Modifier.align(Alignment.Center).padding(16.dp)
108+
)
109+
} else {
110+
A2UISurface(
111+
surfaceId = "contact-card",
112+
state = surfaceState,
113+
onUserAction = { action, src, contextMap ->
114+
Log.d("A2UI", "Action: ${action.name} from $src")
115+
116+
val actionData = kotlinx.serialization.json.JsonObject(contextMap)
117+
118+
scope.launch(Dispatchers.IO) {
119+
try {
120+
val updates = a2aClient.sendEvent(actionData)
121+
withContext(Dispatchers.Main) {
122+
updates.forEach { surfaceState.applyUpdate(it) }
123+
}
124+
} catch (e: Exception) {
125+
e.printStackTrace()
126+
}
99127
}
100-
} catch (e: Exception) {
101-
e.printStackTrace()
102128
}
103-
}
129+
)
104130
}
105-
)
131+
}
106132
}
107133
}

0 commit comments

Comments
 (0)