Skip to content

Commit a3de5a7

Browse files
committed
Refactor Imagen sample with improved UI and state management
* Refactor the Imagen sample to use a sealed UI state (`Initial`, `Loading`, `ImageGenerated`, `Error`) * Break down `ImagenScreen` into smaller, reusable composables with previews * Add error handling for the image generation API call * Improve keyboard handling by using `adjustResize` and IME insets * Allow image generation via the keyboard's 'Send' action * Update dependencies to latest versions
1 parent eb74abc commit a3de5a7

File tree

6 files changed

+211
-85
lines changed

6 files changed

+211
-85
lines changed

ai-catalog/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<activity
1818
android:name=".MainActivity"
1919
android:exported="true"
20+
android:windowSoftInputMode="adjustResize"
2021
android:theme="@style/Theme.AISampleCatalog">
2122
<intent-filter>
2223
<action android:name="android.intent.action.MAIN" />

ai-catalog/gradle/libs.versions.toml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[versions]
2-
agp = "8.8.0"
2+
agp = "8.8.2"
33
coilCompose = "3.1.0"
44
firebaseBom = "33.14.0"
55
mlkitGenAi = "1.0.0-beta1"
@@ -11,13 +11,13 @@ espressoCore = "3.6.1"
1111
kotlinxCoroutinesGuava = "1.10.2"
1212
kotlinxSerializationJson = "1.6.2"
1313
lifecycleRuntimeKtx = "2.8.7"
14-
activityCompose = "1.10.0"
15-
composeBom = "2025.01.00"
16-
navigationCompose = "2.8.5"
17-
navigationRuntimeKtx = "2.8.5"
14+
activityCompose = "1.10.1"
15+
composeBom = "2025.06.01"
16+
navigationCompose = "2.9.0"
17+
navigationRuntimeKtx = "2.9.0"
1818
appcompat = "1.7.0"
1919
googleGmsGoogleServices = "4.4.2"
20-
hilt = "2.51.1"
20+
hilt = "2.56.2"
2121
hiltNavigationCompose = "1.2.0"
2222
ksp = "2.1.0-1.0.29"
2323
runtimeLivedata = "1.7.6"
@@ -26,6 +26,8 @@ media3 = "1.6.1"
2626
firebaseCommonKtx = "21.0.0"
2727
uiToolingPreviewAndroid = "1.8.1"
2828
spotless = "7.0.4"
29+
uiToolingPreview = "1.8.3"
30+
uiTooling = "1.8.3"
2931

3032
[libraries]
3133
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -64,6 +66,8 @@ androidx-material3-android = { group = "androidx.compose.material3", name = "mat
6466
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
6567
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }
6668
androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "uiToolingPreviewAndroid" }
69+
ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "uiToolingPreview" }
70+
ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
6771

6872
[plugins]
6973
android-application = { id = "com.android.application", version.ref = "agp" }

ai-catalog/samples/imagen/build.gradle.kts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,19 @@ android {
4444
)
4545
}
4646
}
47+
4748
compileOptions {
4849
sourceCompatibility = JavaVersion.VERSION_17
4950
targetCompatibility = JavaVersion.VERSION_17
5051
}
51-
composeOptions {
52-
kotlinCompilerExtensionVersion = "1.5.15"
53-
}
52+
5453
kotlinOptions {
5554
jvmTarget = "17"
5655
}
56+
57+
lint {
58+
warningsAsErrors = true
59+
}
5760
}
5861

5962
dependencies {
@@ -68,6 +71,8 @@ dependencies {
6871
implementation(libs.hilt.android)
6972
implementation(libs.hilt.navigation.compose)
7073
implementation(libs.androidx.runtime.livedata)
74+
implementation(libs.ui.tooling.preview)
75+
debugImplementation(libs.ui.tooling)
7176
ksp(libs.hilt.compiler)
7277

7378
testImplementation(libs.junit)

ai-catalog/samples/imagen/src/main/java/com/android/ai/samples/imagen/ImagenScreen.kt

Lines changed: 169 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,30 @@
1515
*/
1616
package com.android.ai.samples.imagen
1717

18-
import android.content.Context
1918
import android.content.Intent
20-
import android.net.Uri
2119
import androidx.compose.foundation.Image
20+
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
2221
import androidx.compose.foundation.layout.Column
23-
import androidx.compose.foundation.layout.Row
2422
import androidx.compose.foundation.layout.Spacer
23+
import androidx.compose.foundation.layout.WindowInsets
24+
import androidx.compose.foundation.layout.aspectRatio
2525
import androidx.compose.foundation.layout.fillMaxSize
2626
import androidx.compose.foundation.layout.fillMaxWidth
2727
import androidx.compose.foundation.layout.height
28+
import androidx.compose.foundation.layout.ime
2829
import androidx.compose.foundation.layout.padding
2930
import androidx.compose.foundation.layout.size
31+
import androidx.compose.foundation.layout.windowInsetsBottomHeight
3032
import androidx.compose.foundation.layout.wrapContentSize
3133
import androidx.compose.foundation.rememberScrollState
34+
import androidx.compose.foundation.text.KeyboardActions
35+
import androidx.compose.foundation.text.KeyboardOptions
3236
import androidx.compose.foundation.verticalScroll
3337
import androidx.compose.material.icons.Icons
3438
import androidx.compose.material.icons.filled.Code
3539
import androidx.compose.material.icons.filled.SmartToy
3640
import androidx.compose.material3.Button
41+
import androidx.compose.material3.ButtonDefaults
3742
import androidx.compose.material3.Card
3843
import androidx.compose.material3.ExperimentalMaterial3Api
3944
import androidx.compose.material3.Icon
@@ -46,30 +51,40 @@ import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
4651
import androidx.compose.runtime.Composable
4752
import androidx.compose.runtime.collectAsState
4853
import androidx.compose.runtime.getValue
49-
import androidx.compose.runtime.livedata.observeAsState
5054
import androidx.compose.runtime.mutableStateOf
51-
import androidx.compose.runtime.remember
55+
import androidx.compose.runtime.saveable.rememberSaveable
5256
import androidx.compose.runtime.setValue
5357
import androidx.compose.ui.Alignment
5458
import androidx.compose.ui.Modifier
5559
import androidx.compose.ui.graphics.asImageBitmap
5660
import androidx.compose.ui.layout.ContentScale
5761
import androidx.compose.ui.platform.LocalContext
5862
import androidx.compose.ui.res.stringResource
63+
import androidx.compose.ui.text.input.ImeAction
5964
import androidx.compose.ui.text.style.TextAlign
65+
import androidx.compose.ui.tooling.preview.Preview
6066
import androidx.compose.ui.unit.dp
61-
import androidx.compose.ui.unit.sp
67+
import androidx.core.net.toUri
6268
import androidx.hilt.navigation.compose.hiltViewModel
6369

6470
@OptIn(ExperimentalMaterial3Api::class)
6571
@Composable
6672
fun ImagenScreen(viewModel: ImagenViewModel = hiltViewModel()) {
67-
val context = LocalContext.current
68-
val isGenerating by viewModel.isGenerating.observeAsState(false)
69-
val generatedBitmap by viewModel.imageGenerated.collectAsState()
73+
val uiState: ImagenUIState by viewModel.uiState.collectAsState()
7074

71-
val placeholder = stringResource(R.string.placeholder_prompt)
72-
var editTextValue by remember { mutableStateOf(placeholder) }
75+
ImagenScreen(
76+
uiState = uiState,
77+
onGenerateClick = viewModel::generateImage,
78+
)
79+
}
80+
81+
@Composable
82+
@OptIn(ExperimentalMaterial3Api::class)
83+
private fun ImagenScreen(
84+
uiState: ImagenUIState,
85+
onGenerateClick: (String) -> Unit
86+
) {
87+
val isGenerating = uiState is ImagenUIState.Loading
7388

7489
Scaffold(
7590
modifier = Modifier,
@@ -83,79 +98,173 @@ fun ImagenScreen(viewModel: ImagenViewModel = hiltViewModel()) {
8398
Text(text = stringResource(R.string.title_image_generation_screen))
8499
},
85100
actions = {
86-
SeeCodeButton(context)
101+
SeeCodeButton()
87102
},
88103
)
89104
},
90105
) { innerPadding ->
91106
Column(
92107
Modifier
93-
.padding(12.dp)
94108
.verticalScroll(rememberScrollState())
109+
.padding(16.dp)
95110
.padding(innerPadding),
96111
) {
97-
Card(
98-
modifier = Modifier.size(
99-
width = 400.dp,
100-
height = 400.dp,
101-
).align(Alignment.CenterHorizontally),
102-
) {
103-
generatedBitmap?.let {
104-
Image(
105-
bitmap = it.asImageBitmap(),
106-
contentDescription = "Picture",
107-
contentScale = ContentScale.Fit,
108-
modifier = Modifier.fillMaxSize(),
109-
)
110-
}
111-
if (isGenerating) {
112-
Text(
113-
text = stringResource(R.string.generating_label),
114-
modifier = Modifier
115-
.fillMaxSize()
116-
.wrapContentSize(Alignment.Center),
117-
textAlign = TextAlign.Center,
118-
)
119-
}
120-
}
121-
Spacer(modifier = Modifier.height(24.dp))
122-
TextField(
123-
value = editTextValue,
124-
onValueChange = { editTextValue = it },
125-
label = { Text(stringResource(R.string.prompt_label)) },
126-
modifier = Modifier.fillMaxWidth(),
112+
GeneratedContent(
113+
uiState = uiState,
114+
modifier = Modifier
115+
.fillMaxWidth()
116+
.aspectRatio(1f)
127117
)
128-
Row {
129-
Button(
130-
modifier = Modifier.padding(vertical = 8.dp),
131-
onClick = {
132-
viewModel.generateImage(editTextValue)
133-
},
134-
enabled = !isGenerating,
135-
) {
136-
Icon(Icons.Default.SmartToy, contentDescription = "Robot")
137-
Text(modifier = Modifier.padding(start = 8.dp), text = stringResource(R.string.generate_button))
138-
}
118+
119+
Spacer(modifier = Modifier.height(16.dp))
120+
121+
GenerationInput(
122+
onGenerateClick = onGenerateClick,
123+
enabled = !isGenerating,
124+
modifier = Modifier.fillMaxSize(),
125+
)
126+
127+
// Ensure the screen scrolls when the keyboard appears
128+
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
129+
}
130+
}
131+
}
132+
133+
@Composable
134+
private fun GeneratedContent(uiState: ImagenUIState, modifier: Modifier = Modifier) {
135+
Card(
136+
modifier = modifier,
137+
) {
138+
when (uiState) {
139+
ImagenUIState.Initial -> {
140+
// no-op
141+
}
142+
143+
ImagenUIState.Loading -> {
144+
Text(
145+
text = stringResource(R.string.generating_label),
146+
modifier = Modifier
147+
.fillMaxSize()
148+
.wrapContentSize(Alignment.Center),
149+
textAlign = TextAlign.Center,
150+
)
151+
}
152+
153+
is ImagenUIState.ImageGenerated -> {
154+
Image(
155+
bitmap = uiState.bitmap.asImageBitmap(),
156+
contentDescription = uiState.contentDescription,
157+
contentScale = ContentScale.Fit,
158+
modifier = Modifier.fillMaxSize(),
159+
)
160+
}
161+
162+
is ImagenUIState.Error -> {
163+
Text(
164+
text = uiState.message,
165+
modifier = Modifier
166+
.fillMaxSize()
167+
.wrapContentSize(Alignment.Center),
168+
textAlign = TextAlign.Center,
169+
)
139170
}
140171
}
141172
}
142173
}
143174

144175
@Composable
145-
fun SeeCodeButton(context: Context) {
176+
private fun GenerationInput(onGenerateClick: (String) -> Unit, enabled: Boolean, modifier: Modifier = Modifier) {
177+
val placeholder = stringResource(R.string.placeholder_prompt)
178+
var textFieldValue by rememberSaveable { mutableStateOf(placeholder) }
179+
180+
Column(
181+
verticalArrangement = spacedBy(8.dp),
182+
modifier = modifier,
183+
) {
184+
TextField(
185+
value = textFieldValue,
186+
onValueChange = { textFieldValue = it },
187+
label = { Text(stringResource(R.string.prompt_label)) },
188+
modifier = Modifier.fillMaxWidth(),
189+
enabled = enabled,
190+
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
191+
keyboardActions = KeyboardActions(
192+
onSend = {
193+
onGenerateClick(textFieldValue)
194+
},
195+
),
196+
)
197+
Button(
198+
onClick = {
199+
onGenerateClick(textFieldValue)
200+
},
201+
enabled = enabled,
202+
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
203+
modifier = modifier.fillMaxWidth(),
204+
) {
205+
Icon(
206+
Icons.Default.SmartToy,
207+
contentDescription = null,
208+
modifier = Modifier.size(ButtonDefaults.IconSize),
209+
)
210+
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
211+
Text(text = stringResource(R.string.generate_button))
212+
}
213+
}
214+
}
215+
216+
@Composable
217+
private fun SeeCodeButton() {
218+
val context = LocalContext.current
146219
val githubLink = "https://github.com/android/ai-samples/tree/main/ai-catalog/samples/imagen"
147220
Button(
148221
onClick = {
149-
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubLink))
222+
val intent = Intent(Intent.ACTION_VIEW, githubLink.toUri())
150223
context.startActivity(intent)
151224
},
152-
modifier = Modifier.padding(end = 8.dp),
225+
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
153226
) {
154-
Icon(Icons.Filled.Code, contentDescription = "See code")
227+
Icon(Icons.Filled.Code, contentDescription = null)
228+
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
155229
Text(
156-
modifier = Modifier.padding(start = 8.dp),
157-
fontSize = 12.sp,
158230
text = stringResource(R.string.see_code),
159231
)
160232
}
161233
}
234+
235+
@Preview
236+
@Composable
237+
@OptIn(ExperimentalMaterial3Api::class)
238+
private fun ImagenScreenPreview() {
239+
ImagenScreen(
240+
uiState = ImagenUIState.Initial,
241+
onGenerateClick = {},
242+
)
243+
}
244+
245+
@Preview
246+
@Composable
247+
private fun GeneratedContentPreview() {
248+
GeneratedContent(
249+
uiState = ImagenUIState.Initial,
250+
modifier = Modifier.size(400.dp),
251+
)
252+
}
253+
254+
@Preview
255+
@Composable
256+
private fun GeneratedContentLoadingPreview() {
257+
GeneratedContent(
258+
uiState = ImagenUIState.Loading,
259+
modifier = Modifier.size(400.dp),
260+
)
261+
}
262+
263+
@Preview
264+
@Composable
265+
private fun GenerationInputPreview() {
266+
GenerationInput(
267+
onGenerateClick = {},
268+
enabled = true,
269+
)
270+
}

0 commit comments

Comments
 (0)