Skip to content

Commit e092b9b

Browse files
authored
Refactor Gemini Chatbot sample to use UI State and handle errors (#54)
Co-authored-by: ksemenova <[email protected]>
1 parent 95bd007 commit e092b9b

File tree

5 files changed

+90
-22
lines changed

5 files changed

+90
-22
lines changed

ai-catalog/gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
agp = "8.8.2"
33
coilCompose = "3.1.0"
44
firebaseBom = "33.14.0"
5+
lifecycleRuntimeCompose = "2.9.1"
56
mlkitGenAi = "1.0.0-beta1"
67
kotlin = "2.1.0"
78
coreKtx = "1.15.0"
@@ -31,6 +32,7 @@ uiTooling = "1.8.3"
3132

3233
[libraries]
3334
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
35+
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" }
3436
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
3537
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
3638
firebase-ai = { group = "com.google.firebase", name = "firebase-ai" }

ai-catalog/samples/gemini-chatbot/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ dependencies {
6565
implementation(libs.hilt.android)
6666
implementation(libs.hilt.navigation.compose)
6767
implementation(libs.androidx.runtime.livedata)
68+
implementation(libs.androidx.lifecycle.runtime.compose)
6869
implementation(libs.androidx.material3.android)
6970
ksp(libs.hilt.compiler)
7071

ai-catalog/samples/gemini-chatbot/src/main/java/com/android/ai/samples/geminichatbot/GeminiChatbotScreen.kt

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package com.android.ai.samples.geminichatbot
1717

1818
import android.content.Intent
19-
import android.net.Uri
2019
import androidx.compose.foundation.layout.Arrangement
2120
import androidx.compose.foundation.layout.Column
2221
import androidx.compose.foundation.layout.PaddingValues
@@ -32,7 +31,9 @@ import androidx.compose.foundation.lazy.LazyColumn
3231
import androidx.compose.foundation.lazy.items
3332
import androidx.compose.material.icons.Icons
3433
import androidx.compose.material.icons.filled.Code
34+
import androidx.compose.material3.AlertDialog
3535
import androidx.compose.material3.Button
36+
import androidx.compose.material3.CircularProgressIndicator
3637
import androidx.compose.material3.ExperimentalMaterial3Api
3738
import androidx.compose.material3.Icon
3839
import androidx.compose.material3.MaterialTheme
@@ -44,7 +45,6 @@ import androidx.compose.material3.TopAppBarDefaults
4445
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
4546
import androidx.compose.material3.rememberTopAppBarState
4647
import androidx.compose.runtime.Composable
47-
import androidx.compose.runtime.collectAsState
4848
import androidx.compose.runtime.getValue
4949
import androidx.compose.runtime.mutableStateOf
5050
import androidx.compose.runtime.saveable.rememberSaveable
@@ -59,14 +59,16 @@ import androidx.compose.ui.unit.Dp
5959
import androidx.compose.ui.unit.LayoutDirection
6060
import androidx.compose.ui.unit.dp
6161
import androidx.compose.ui.unit.sp
62+
import androidx.core.net.toUri
6263
import androidx.hilt.navigation.compose.hiltViewModel
64+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
6365

6466
@OptIn(ExperimentalMaterial3Api::class)
6567
@Composable
6668
fun GeminiChatbotScreen(viewModel: GeminiChatbotViewModel = hiltViewModel()) {
6769
val topAppBarState = rememberTopAppBarState()
6870
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topAppBarState)
69-
val messages by viewModel.messageList.collectAsState()
71+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
7072
var message by rememberSaveable { mutableStateOf("") }
7173

7274
Scaffold(
@@ -88,15 +90,50 @@ fun GeminiChatbotScreen(viewModel: GeminiChatbotViewModel = hiltViewModel()) {
8890
)
8991
},
9092
) { innerPadding ->
93+
9194
Column {
9295
val layoutDirection = LocalLayoutDirection.current
96+
97+
val messages = when (val state = uiState) {
98+
is GeminiChatbotUiState.Initial -> emptyList()
99+
is GeminiChatbotUiState.Generating -> state.messages
100+
is GeminiChatbotUiState.Success -> state.messages
101+
is GeminiChatbotUiState.Error -> state.messages
102+
}
103+
93104
MessageList(
94105
modifier = Modifier
95106
.fillMaxWidth()
96107
.weight(1f),
97-
messages = messages.sortedBy { - it.timestamp },
108+
messages = messages.sortedByDescending { it.timestamp },
98109
contentPadding = innerPadding,
99110
)
111+
112+
when (val state = uiState) {
113+
is GeminiChatbotUiState.Generating -> {
114+
CircularProgressIndicator(
115+
modifier = Modifier
116+
.padding(vertical = 8.dp)
117+
.align(Alignment.CenterHorizontally),
118+
)
119+
}
120+
121+
is GeminiChatbotUiState.Error -> {
122+
AlertDialog(
123+
onDismissRequest = { viewModel.dismissError() },
124+
title = { Text(text = stringResource(R.string.error)) },
125+
text = { Text(text = state.errorMessage) },
126+
confirmButton = {
127+
Button(onClick = { viewModel.dismissError() }) {
128+
Text(text = stringResource(R.string.dismiss_button))
129+
}
130+
},
131+
)
132+
}
133+
134+
else -> { /* No additional UI for Initial or Success states */ }
135+
}
136+
100137
InputBar(
101138
value = message,
102139
placeholder = stringResource(R.string.geminichatbot_input_placeholder),
@@ -108,7 +145,7 @@ fun GeminiChatbotScreen(viewModel: GeminiChatbotViewModel = hiltViewModel()) {
108145
message = ""
109146
},
110147
contentPadding = innerPadding.copy(layoutDirection, top = 0.dp),
111-
sendEnabled = true,
148+
sendEnabled = uiState !is GeminiChatbotUiState.Generating,
112149
)
113150
}
114151
}
@@ -176,7 +213,7 @@ fun SeeCodeButton() {
176213

177214
Button(
178215
onClick = {
179-
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubLink))
216+
val intent = Intent(Intent.ACTION_VIEW, githubLink.toUri())
180217
context.startActivity(intent)
181218
},
182219
modifier = Modifier.padding(end = 8.dp),

ai-catalog/samples/gemini-chatbot/src/main/java/com/android/ai/samples/geminichatbot/GeminiChatbotViewModel.kt

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,23 @@ import com.google.firebase.ai.type.generationConfig
2828
import javax.inject.Inject
2929
import kotlinx.coroutines.flow.MutableStateFlow
3030
import kotlinx.coroutines.flow.StateFlow
31+
import kotlinx.coroutines.flow.asStateFlow
3132
import kotlinx.coroutines.launch
3233

34+
sealed class GeminiChatbotUiState {
35+
data object Initial : GeminiChatbotUiState()
36+
data class Generating(val messages: List<ChatMessage>) : GeminiChatbotUiState()
37+
data class Success(val messages: List<ChatMessage>) : GeminiChatbotUiState()
38+
data class Error(
39+
val errorMessage: String,
40+
val messages: List<ChatMessage>,
41+
) : GeminiChatbotUiState()
42+
}
43+
3344
class GeminiChatbotViewModel @Inject constructor() : ViewModel() {
3445

35-
private val _messageList = MutableStateFlow(mutableListOf<ChatMessage>())
36-
val messageList: StateFlow<List<ChatMessage>> = _messageList
46+
private val _uiState = MutableStateFlow<GeminiChatbotUiState>(GeminiChatbotUiState.Initial)
47+
val uiState: StateFlow<GeminiChatbotUiState> = _uiState.asStateFlow()
3748

3849
private val generativeModel by lazy {
3950
Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel(
@@ -59,21 +70,24 @@ class GeminiChatbotViewModel @Inject constructor() : ViewModel() {
5970
private val chat = generativeModel.startChat()
6071

6172
fun sendMessage(message: String) {
62-
viewModelScope.launch {
63-
_messageList.value.add(
64-
ChatMessage(
65-
message,
66-
System.currentTimeMillis(),
67-
false,
68-
null,
69-
),
70-
)
73+
val currentMessages =
74+
when (val state = _uiState.value) {
75+
is GeminiChatbotUiState.Success -> state.messages
76+
is GeminiChatbotUiState.Error -> state.messages
77+
else -> emptyList()
78+
}
7179

72-
val response = chat.sendMessage(message)
80+
val newMessages =
81+
currentMessages.toMutableList().apply {
82+
add(ChatMessage(message, System.currentTimeMillis(), false, null))
83+
}
84+
viewModelScope.launch {
85+
try {
86+
_uiState.value = GeminiChatbotUiState.Generating(newMessages)
7387

74-
response.text?.let {
75-
_messageList.value = _messageList.value.toMutableList().apply {
76-
add(
88+
val response = chat.sendMessage(message)
89+
response.text?.let {
90+
newMessages.add(
7791
ChatMessage(
7892
it.trim(),
7993
System.currentTimeMillis(),
@@ -82,7 +96,19 @@ class GeminiChatbotViewModel @Inject constructor() : ViewModel() {
8296
),
8397
)
8498
}
99+
_uiState.value = GeminiChatbotUiState.Success(newMessages)
100+
} catch (e: Exception) {
101+
_uiState.value =
102+
GeminiChatbotUiState.Error(
103+
e.localizedMessage ?: "Something went wrong, try again",
104+
newMessages,
105+
)
85106
}
86107
}
87108
}
109+
110+
fun dismissError() {
111+
val errorState = _uiState.value as? GeminiChatbotUiState.Error ?: return
112+
_uiState.value = GeminiChatbotUiState.Success(errorState.messages)
113+
}
88114
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
3-
<string name="geminichatbot_title_bar">Gemini chatbot</string>
3+
<string name="geminichatbot_title_bar">Gemini Chatbot</string>
44
<string name="geminichatbot_input_placeholder">Type your message</string>
55
<string name="see_code">See code</string>
6+
<string name="dismiss_button">Dismiss</string>
7+
<string name="error">Error</string>
68
</resources>

0 commit comments

Comments
 (0)