Skip to content

Commit f733d09

Browse files
authored
add Imagen screen to quickstart (#2657)
1 parent 596999f commit f733d09

File tree

7 files changed

+299
-2
lines changed

7 files changed

+299
-2
lines changed

vertexai/app/src/main/kotlin/com/google/firebase/quickstart/vertexai/GenerativeAiViewModelFactory.kt

+26-1
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,24 @@ import com.google.firebase.Firebase
2323
import com.google.firebase.quickstart.vertexai.feature.audio.AudioViewModel
2424
import com.google.firebase.quickstart.vertexai.feature.chat.ChatViewModel
2525
import com.google.firebase.quickstart.vertexai.feature.functioncalling.FunctionsChatViewModel
26+
import com.google.firebase.quickstart.vertexai.feature.image.ImagenViewModel
2627
import com.google.firebase.quickstart.vertexai.feature.multimodal.PhotoReasoningViewModel
2728
import com.google.firebase.quickstart.vertexai.feature.text.SummarizeViewModel
29+
import com.google.firebase.vertexai.type.FunctionDeclaration
30+
import com.google.firebase.vertexai.type.ImagenAspectRatio
31+
import com.google.firebase.vertexai.type.ImagenImageFormat
32+
import com.google.firebase.vertexai.type.ImagenPersonFilterLevel
33+
import com.google.firebase.vertexai.type.ImagenSafetyFilterLevel
34+
import com.google.firebase.vertexai.type.ImagenSafetySettings
35+
import com.google.firebase.vertexai.type.PublicPreviewAPI
36+
import com.google.firebase.vertexai.type.SafetySetting
2837
import com.google.firebase.vertexai.type.Schema
2938
import com.google.firebase.vertexai.type.Tool
30-
import com.google.firebase.vertexai.type.FunctionDeclaration
3139
import com.google.firebase.vertexai.type.generationConfig
40+
import com.google.firebase.vertexai.type.imagenGenerationConfig
3241
import com.google.firebase.vertexai.vertexAI
3342

43+
@OptIn(PublicPreviewAPI::class)
3444
val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
3545
override fun <T : ViewModel> create(
3646
viewModelClass: Class<T>,
@@ -106,6 +116,21 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
106116
AudioViewModel(generativeModel)
107117
}
108118

119+
isAssignableFrom(ImagenViewModel::class.java) -> {
120+
val generationConfig = imagenGenerationConfig {
121+
numberOfImages = 1
122+
aspectRatio = ImagenAspectRatio.PORTRAIT_3x4
123+
imageFormat = ImagenImageFormat.png()
124+
}
125+
val safetySettings = ImagenSafetySettings(
126+
safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
127+
personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
128+
)
129+
val imagenModel = Firebase.vertexAI.imagenModel(
130+
"imagen-3.0-generate-002", generationConfig, safetySettings)
131+
ImagenViewModel(imagenModel)
132+
}
133+
109134
else ->
110135
throw IllegalArgumentException("Unknown ViewModel class: ${viewModelClass.name}")
111136
}

vertexai/app/src/main/kotlin/com/google/firebase/quickstart/vertexai/MainActivity.kt

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import androidx.navigation.compose.rememberNavController
2929
import com.google.firebase.quickstart.vertexai.feature.audio.AudioRoute
3030
import com.google.firebase.quickstart.vertexai.feature.chat.ChatRoute
3131
import com.google.firebase.quickstart.vertexai.feature.functioncalling.FunctionsChatRoute
32+
import com.google.firebase.quickstart.vertexai.feature.image.ImagenRoute
3233
import com.google.firebase.quickstart.vertexai.feature.multimodal.PhotoReasoningRoute
3334
import com.google.firebase.quickstart.vertexai.feature.text.SummarizeRoute
3435
import com.google.firebase.quickstart.vertexai.ui.theme.GenerativeAISample
@@ -68,6 +69,9 @@ class MainActivity : ComponentActivity() {
6869
composable("audio") {
6970
AudioRoute()
7071
}
72+
composable("images") {
73+
ImagenRoute()
74+
}
7175
}
7276
}
7377
}

vertexai/app/src/main/kotlin/com/google/firebase/quickstart/vertexai/MenuScreen.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ fun MenuScreen(
4747
MenuItem("photo_reasoning", R.string.menu_reason_title, R.string.menu_reason_description),
4848
MenuItem("chat", R.string.menu_chat_title, R.string.menu_chat_description),
4949
MenuItem("functions_chat", R.string.menu_functions_title, R.string.menu_functions_description),
50-
MenuItem("audio", R.string.menu_audio_title, R.string.menu_audio_description)
50+
MenuItem("audio", R.string.menu_audio_title, R.string.menu_audio_description),
51+
MenuItem("images", R.string.menu_imagen_title, R.string.menu_imagen_description)
5152
)
5253

5354
LazyColumn(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.quickstart.vertexai.feature.image
18+
19+
import androidx.compose.foundation.Image
20+
import androidx.compose.foundation.layout.Box
21+
import androidx.compose.foundation.layout.Column
22+
import androidx.compose.foundation.layout.fillMaxWidth
23+
import androidx.compose.foundation.layout.padding
24+
import androidx.compose.foundation.rememberScrollState
25+
import androidx.compose.foundation.verticalScroll
26+
import androidx.compose.material3.Card
27+
import androidx.compose.material3.CardDefaults
28+
import androidx.compose.material3.CircularProgressIndicator
29+
import androidx.compose.material3.ElevatedCard
30+
import androidx.compose.material3.MaterialTheme
31+
import androidx.compose.material3.OutlinedTextField
32+
import androidx.compose.material3.Text
33+
import androidx.compose.material3.TextButton
34+
import androidx.compose.runtime.Composable
35+
import androidx.compose.runtime.collectAsState
36+
import androidx.compose.runtime.getValue
37+
import androidx.compose.runtime.mutableStateOf
38+
import androidx.compose.runtime.saveable.rememberSaveable
39+
import androidx.compose.runtime.setValue
40+
import androidx.compose.ui.Alignment
41+
import androidx.compose.ui.Modifier
42+
import androidx.compose.ui.graphics.asImageBitmap
43+
import androidx.compose.ui.res.stringResource
44+
import androidx.compose.ui.tooling.preview.Preview
45+
import androidx.compose.ui.unit.dp
46+
import androidx.lifecycle.viewmodel.compose.viewModel
47+
import com.google.firebase.quickstart.vertexai.GenerativeViewModelFactory
48+
import com.google.firebase.quickstart.vertexai.R
49+
import com.google.firebase.quickstart.vertexai.ui.theme.GenerativeAISample
50+
51+
@Composable
52+
internal fun ImagenRoute(
53+
imagenViewModel: ImagenViewModel = viewModel(factory = GenerativeViewModelFactory)
54+
) {
55+
val imagenUiState by imagenViewModel.uiState.collectAsState()
56+
57+
ImagenScreen(imagenUiState, onImagenClicked = { inputText ->
58+
imagenViewModel.generateImage(inputText)
59+
})
60+
}
61+
62+
@Composable
63+
fun ImagenScreen(
64+
uiState: ImagenUiState = ImagenUiState.Loading,
65+
onImagenClicked: (String) -> Unit = {}
66+
) {
67+
var imagenPrompt by rememberSaveable { mutableStateOf("") }
68+
69+
Column(
70+
modifier = Modifier
71+
.verticalScroll(rememberScrollState())
72+
) {
73+
ElevatedCard(
74+
modifier = Modifier
75+
.padding(all = 16.dp)
76+
.fillMaxWidth(),
77+
shape = MaterialTheme.shapes.large
78+
) {
79+
OutlinedTextField(
80+
value = imagenPrompt,
81+
label = { Text(stringResource(R.string.imagen_label)) },
82+
placeholder = { Text(stringResource(R.string.imagen_hint)) },
83+
onValueChange = { imagenPrompt = it },
84+
modifier = Modifier
85+
.padding(16.dp)
86+
.fillMaxWidth()
87+
)
88+
TextButton(
89+
onClick = {
90+
if (imagenPrompt.isNotBlank()) {
91+
onImagenClicked(imagenPrompt)
92+
}
93+
},
94+
modifier = Modifier
95+
.padding(end = 16.dp, bottom = 16.dp)
96+
.align(Alignment.End)
97+
) {
98+
Text(stringResource(R.string.action_go))
99+
}
100+
}
101+
102+
when (uiState) {
103+
ImagenUiState.Initial -> {
104+
// Nothing is shown
105+
}
106+
107+
ImagenUiState.Loading -> {
108+
Box(
109+
contentAlignment = Alignment.Center,
110+
modifier = Modifier
111+
.padding(all = 8.dp)
112+
.align(Alignment.CenterHorizontally)
113+
) {
114+
CircularProgressIndicator()
115+
}
116+
}
117+
118+
is ImagenUiState.Success -> {
119+
Card(
120+
modifier = Modifier
121+
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
122+
.fillMaxWidth(),
123+
shape = MaterialTheme.shapes.large,
124+
colors = CardDefaults.cardColors(
125+
containerColor = MaterialTheme.colorScheme.onSecondaryContainer
126+
)
127+
) {
128+
Image(bitmap = uiState.image.asImageBitmap(), "")
129+
}
130+
}
131+
132+
is ImagenUiState.Error -> {
133+
Card(
134+
modifier = Modifier
135+
.padding(horizontal = 16.dp)
136+
.fillMaxWidth(),
137+
shape = MaterialTheme.shapes.large,
138+
colors = CardDefaults.cardColors(
139+
containerColor = MaterialTheme.colorScheme.errorContainer
140+
)
141+
) {
142+
Text(
143+
text = uiState.errorMessage,
144+
color = MaterialTheme.colorScheme.error,
145+
modifier = Modifier.padding(all = 16.dp)
146+
)
147+
}
148+
}
149+
}
150+
}
151+
}
152+
153+
@Composable
154+
@Preview(showSystemUi = true)
155+
fun ImagenScreenPreview() {
156+
GenerativeAISample(darkTheme = true) {
157+
ImagenScreen()
158+
}
159+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.quickstart.vertexai.feature.image
18+
19+
import android.graphics.Bitmap
20+
21+
/**
22+
* A sealed hierarchy describing the state of the text generation.
23+
*/
24+
sealed interface ImagenUiState {
25+
26+
/**
27+
* Empty state when the screen is first shown
28+
*/
29+
data object Initial : ImagenUiState
30+
31+
/**
32+
* Still loading
33+
*/
34+
data object Loading : ImagenUiState
35+
36+
/**
37+
* Text has been generated
38+
*/
39+
data class Success(
40+
val image: Bitmap
41+
) : ImagenUiState
42+
43+
/**
44+
* There was an error generating an image
45+
*/
46+
data class Error(
47+
val errorMessage: String
48+
) : ImagenUiState
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.quickstart.vertexai.feature.image
18+
19+
import androidx.lifecycle.ViewModel
20+
import androidx.lifecycle.viewModelScope
21+
import com.google.firebase.vertexai.ImagenModel
22+
import com.google.firebase.vertexai.type.PublicPreviewAPI
23+
import kotlinx.coroutines.flow.MutableStateFlow
24+
import kotlinx.coroutines.flow.StateFlow
25+
import kotlinx.coroutines.flow.asStateFlow
26+
import kotlinx.coroutines.launch
27+
28+
@OptIn(PublicPreviewAPI::class)
29+
class ImagenViewModel(
30+
private val imageModel: ImagenModel
31+
) : ViewModel() {
32+
33+
private val _uiState: MutableStateFlow<ImagenUiState> =
34+
MutableStateFlow(ImagenUiState.Initial)
35+
val uiState: StateFlow<ImagenUiState> = _uiState.asStateFlow()
36+
37+
fun generateImage(inputText: String) {
38+
_uiState.value = ImagenUiState.Loading
39+
40+
viewModelScope.launch {
41+
// Non-streaming
42+
try {
43+
val imageResponse = imageModel.generateImages(
44+
inputText
45+
)
46+
_uiState.value =
47+
ImagenUiState.Success(imageResponse.images.first().asBitmap())
48+
} catch (e: Exception) {
49+
_uiState.value = ImagenUiState.Error(e.localizedMessage ?: "")
50+
}
51+
}
52+
}
53+
}

vertexai/app/src/main/res/values/strings.xml

+6
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,17 @@
2626
<string name="menu_chat_description">Sample app demonstrating a conversational UI</string>
2727
<string name="menu_audio_title">Generate text from text-and-audio input</string>
2828
<string name="menu_audio_description">Sample app for recording audio and generating text from it</string>
29+
<string name="menu_imagen_title">Generate images from a text-only input</string>
30+
<string name="menu_imagen_description">Sample app demonstrating how to use Imagen 3 for image generation</string>
2931

3032
<!-- Summarize sample strings -->
3133
<string name="summarize_label">Text</string>
3234
<string name="summarize_hint">Enter text to summarize</string>
3335

36+
<!-- Generate Image String -->
37+
<string name="imagen_label">Prompt</string>
38+
<string name="imagen_hint">Enter text to generate an image</string>
39+
3440
<!-- Photo Reasoning sample strings -->
3541
<string name="reason_label">Question</string>
3642
<string name="reason_hint">Upload an image and then ask a question</string>

0 commit comments

Comments
 (0)