Skip to content

Commit 5e6e1d2

Browse files
authored
Merge pull request #51 from android/ben/imagen_cleanup
Refactor Imagen sample with improved UI and state management
2 parents eb74abc + 1a348c7 commit 5e6e1d2

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: 166 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
@@ -44,32 +49,39 @@ import androidx.compose.material3.TextField
4449
import androidx.compose.material3.TopAppBar
4550
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
4651
import androidx.compose.runtime.Composable
47-
import androidx.compose.runtime.collectAsState
4852
import androidx.compose.runtime.getValue
49-
import androidx.compose.runtime.livedata.observeAsState
5053
import androidx.compose.runtime.mutableStateOf
51-
import androidx.compose.runtime.remember
54+
import androidx.compose.runtime.saveable.rememberSaveable
5255
import androidx.compose.runtime.setValue
5356
import androidx.compose.ui.Alignment
5457
import androidx.compose.ui.Modifier
5558
import androidx.compose.ui.graphics.asImageBitmap
5659
import androidx.compose.ui.layout.ContentScale
5760
import androidx.compose.ui.platform.LocalContext
5861
import androidx.compose.ui.res.stringResource
62+
import androidx.compose.ui.text.input.ImeAction
5963
import androidx.compose.ui.text.style.TextAlign
64+
import androidx.compose.ui.tooling.preview.Preview
6065
import androidx.compose.ui.unit.dp
61-
import androidx.compose.ui.unit.sp
66+
import androidx.core.net.toUri
6267
import androidx.hilt.navigation.compose.hiltViewModel
68+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.collectAsStateWithLifecycle()
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(uiState: ImagenUIState, onGenerateClick: (String) -> Unit) {
84+
val isGenerating = uiState is ImagenUIState.Loading
7385

7486
Scaffold(
7587
modifier = Modifier,
@@ -83,79 +95,173 @@ fun ImagenScreen(viewModel: ImagenViewModel = hiltViewModel()) {
8395
Text(text = stringResource(R.string.title_image_generation_screen))
8496
},
8597
actions = {
86-
SeeCodeButton(context)
98+
SeeCodeButton()
8799
},
88100
)
89101
},
90102
) { innerPadding ->
91103
Column(
92104
Modifier
93-
.padding(12.dp)
94105
.verticalScroll(rememberScrollState())
106+
.padding(16.dp)
95107
.padding(innerPadding),
96108
) {
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)) },
109+
GeneratedContent(
110+
uiState = uiState,
111+
modifier = Modifier
112+
.fillMaxWidth()
113+
.aspectRatio(1f),
114+
)
115+
116+
Spacer(modifier = Modifier.height(16.dp))
117+
118+
GenerationInput(
119+
onGenerateClick = onGenerateClick,
120+
enabled = !isGenerating,
126121
modifier = Modifier.fillMaxWidth(),
127122
)
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-
}
123+
124+
// Ensure the screen scrolls when the keyboard appears
125+
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
126+
}
127+
}
128+
}
129+
130+
@Composable
131+
private fun GeneratedContent(uiState: ImagenUIState, modifier: Modifier = Modifier) {
132+
Card(
133+
modifier = modifier,
134+
) {
135+
when (uiState) {
136+
ImagenUIState.Initial -> {
137+
// no-op
138+
}
139+
140+
ImagenUIState.Loading -> {
141+
Text(
142+
text = stringResource(R.string.generating_label),
143+
modifier = Modifier
144+
.fillMaxSize()
145+
.wrapContentSize(Alignment.Center),
146+
textAlign = TextAlign.Center,
147+
)
148+
}
149+
150+
is ImagenUIState.ImageGenerated -> {
151+
Image(
152+
bitmap = uiState.bitmap.asImageBitmap(),
153+
contentDescription = uiState.contentDescription,
154+
contentScale = ContentScale.Fit,
155+
modifier = Modifier.fillMaxSize(),
156+
)
157+
}
158+
159+
is ImagenUIState.Error -> {
160+
Text(
161+
text = uiState.message,
162+
modifier = Modifier
163+
.fillMaxSize()
164+
.wrapContentSize(Alignment.Center),
165+
textAlign = TextAlign.Center,
166+
)
139167
}
140168
}
141169
}
142170
}
143171

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

0 commit comments

Comments
 (0)