Skip to content

Commit af47289

Browse files
Gemini multimodal module refactoring
1 parent 0668adc commit af47289

File tree

6 files changed

+126
-49
lines changed

6 files changed

+126
-49
lines changed

ai-catalog/app/src/main/java/com/android/ai/catalog/ui/domain/SampleCatalog.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import androidx.compose.runtime.Composable
2020
import androidx.compose.ui.graphics.Color
2121
import com.android.ai.catalog.R
2222
import com.android.ai.samples.geminichatbot.GeminiChatbotScreen
23-
import com.android.ai.samples.geminimultimodal.GeminiMultimodalScreen
23+
import com.android.ai.samples.geminimultimodal.ui.GeminiMultimodalScreen
2424
import com.android.ai.samples.geminivideosummary.VideoSummarizationScreen
2525
import com.android.ai.samples.genai_image_description.GenAIImageDescriptionScreen
2626
import com.android.ai.samples.genai_summarization.GenAISummarizationScreen
Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,9 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package com.android.ai.samples.geminimultimodal
16+
package com.android.ai.samples.geminimultimodal.data
1717

1818
import android.graphics.Bitmap
19-
import androidx.lifecycle.LiveData
20-
import androidx.lifecycle.MutableLiveData
21-
import androidx.lifecycle.ViewModel
22-
import androidx.lifecycle.viewModelScope
2319
import com.google.firebase.Firebase
2420
import com.google.firebase.ai.ai
2521
import com.google.firebase.ai.type.GenerativeBackend
@@ -29,21 +25,13 @@ import com.google.firebase.ai.type.SafetySetting
2925
import com.google.firebase.ai.type.content
3026
import com.google.firebase.ai.type.generationConfig
3127
import javax.inject.Inject
32-
import kotlinx.coroutines.flow.MutableStateFlow
33-
import kotlinx.coroutines.flow.StateFlow
34-
import kotlinx.coroutines.launch
35-
36-
class GeminiMultimodalViewModel @Inject constructor() : ViewModel() {
37-
38-
private val _textGenerated = MutableStateFlow("")
39-
val textGenerated: StateFlow<String> = _textGenerated
40-
41-
private val _isGenerating = MutableLiveData(false)
42-
val isGenerating: LiveData<Boolean> = _isGenerating
28+
import javax.inject.Singleton
4329

30+
@Singleton
31+
class GeminiDataSource @Inject constructor() {
4432
private val generativeModel by lazy {
4533
Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel(
46-
"gemini-2.0-flash",
34+
"gemini-2.5-flash",
4735
generationConfig = generationConfig {
4836
temperature = 0.9f
4937
topK = 32
@@ -59,20 +47,12 @@ class GeminiMultimodalViewModel @Inject constructor() : ViewModel() {
5947
)
6048
}
6149

62-
fun generate(bitmap: Bitmap, prompt: String) {
63-
_isGenerating.value = true
64-
50+
suspend fun generateText(bitmap: Bitmap, prompt: String): String {
6551
val multimodalPrompt = content {
6652
image(bitmap)
6753
text(prompt)
6854
}
69-
viewModelScope.launch {
70-
val result = generativeModel.generateContent(multimodalPrompt)
71-
72-
result.text?.let {
73-
_textGenerated.value = result.text!!
74-
}
75-
_isGenerating.postValue(false)
76-
}
55+
val result = generativeModel.generateContent(multimodalPrompt)
56+
return result.text ?: ""
7757
}
7858
}
Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package com.android.ai.samples.geminimultimodal
16+
package com.android.ai.samples.geminimultimodal.ui
1717

1818
import android.annotation.SuppressLint
1919
import android.content.Context
@@ -24,12 +24,14 @@ import androidx.activity.compose.rememberLauncherForActivityResult
2424
import androidx.activity.result.contract.ActivityResultContracts.TakePicturePreview
2525
import androidx.compose.foundation.Image
2626
import androidx.compose.foundation.layout.Arrangement
27+
import androidx.compose.foundation.layout.Box
2728
import androidx.compose.foundation.layout.Column
2829
import androidx.compose.foundation.layout.Row
2930
import androidx.compose.foundation.layout.Spacer
3031
import androidx.compose.foundation.layout.fillMaxSize
3132
import androidx.compose.foundation.layout.fillMaxWidth
3233
import androidx.compose.foundation.layout.height
34+
import androidx.compose.foundation.layout.imePadding
3335
import androidx.compose.foundation.layout.padding
3436
import androidx.compose.foundation.layout.size
3537
import androidx.compose.foundation.rememberScrollState
@@ -40,6 +42,7 @@ import androidx.compose.material.icons.filled.Code
4042
import androidx.compose.material.icons.filled.SmartToy
4143
import androidx.compose.material3.Button
4244
import androidx.compose.material3.Card
45+
import androidx.compose.material3.CircularProgressIndicator
4346
import androidx.compose.material3.ExperimentalMaterial3Api
4447
import androidx.compose.material3.Icon
4548
import androidx.compose.material3.MaterialTheme
@@ -49,12 +52,11 @@ import androidx.compose.material3.TextField
4952
import androidx.compose.material3.TopAppBar
5053
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
5154
import androidx.compose.runtime.Composable
52-
import androidx.compose.runtime.collectAsState
5355
import androidx.compose.runtime.getValue
54-
import androidx.compose.runtime.livedata.observeAsState
5556
import androidx.compose.runtime.mutableStateOf
5657
import androidx.compose.runtime.remember
5758
import androidx.compose.runtime.setValue
59+
import androidx.compose.ui.Alignment
5860
import androidx.compose.ui.Modifier
5961
import androidx.compose.ui.graphics.asImageBitmap
6062
import androidx.compose.ui.layout.ContentScale
@@ -63,16 +65,16 @@ import androidx.compose.ui.res.stringResource
6365
import androidx.compose.ui.unit.dp
6466
import androidx.compose.ui.unit.sp
6567
import androidx.hilt.navigation.compose.hiltViewModel
68+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
69+
import com.android.ai.samples.geminimultimodal.R
6670

6771
@OptIn(ExperimentalMaterial3Api::class)
6872
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
6973
@Composable
7074
fun GeminiMultimodalScreen(viewModel: GeminiMultimodalViewModel = hiltViewModel()) {
7175
val context = LocalContext.current
7276
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
73-
val textResponse by viewModel.textGenerated.collectAsState()
74-
val isGenerating by viewModel.isGenerating.observeAsState(false)
75-
var pictureAvailable by remember { mutableStateOf(false) }
77+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
7678

7779
val promptPlaceHolder = stringResource(id = R.string.geminimultimodal_prompt_placeholder)
7880
var editTextValue by remember {
@@ -83,7 +85,6 @@ fun GeminiMultimodalScreen(viewModel: GeminiMultimodalViewModel = hiltViewModel(
8385
val cameraLauncher = rememberLauncherForActivityResult(TakePicturePreview()) { result ->
8486
result?.let {
8587
bitmap = it
86-
pictureAvailable = true
8788
}
8889
}
8990

@@ -106,6 +107,7 @@ fun GeminiMultimodalScreen(viewModel: GeminiMultimodalViewModel = hiltViewModel(
106107
Column(
107108
Modifier
108109
.padding(12.dp)
110+
.imePadding()
109111
.verticalScroll(rememberScrollState())
110112
.padding(innerPadding),
111113
) {
@@ -116,13 +118,24 @@ fun GeminiMultimodalScreen(viewModel: GeminiMultimodalViewModel = hiltViewModel(
116118
height = 450.dp,
117119
),
118120
) {
119-
bitmap?.let {
121+
val currentBitmap = bitmap
122+
if (currentBitmap != null) {
120123
Image(
121-
bitmap = it.asImageBitmap(),
124+
bitmap = currentBitmap.asImageBitmap(),
122125
contentDescription = "Picture",
123126
contentScale = ContentScale.Crop,
124127
modifier = Modifier.fillMaxSize(),
125128
)
129+
} else {
130+
Box(
131+
modifier = Modifier.fillMaxSize(),
132+
contentAlignment = Alignment.Center,
133+
) {
134+
Text(
135+
text = stringResource(id = R.string.geminimultimodal_take_a_picture),
136+
style = MaterialTheme.typography.bodySmall,
137+
)
138+
}
126139
}
127140
}
128141
Spacer(modifier = Modifier.height(6.dp))
@@ -144,11 +157,12 @@ fun GeminiMultimodalScreen(viewModel: GeminiMultimodalViewModel = hiltViewModel(
144157
Spacer(modifier = Modifier.height(8.dp))
145158
Button(
146159
onClick = {
147-
if (bitmap != null) {
148-
viewModel.generate(bitmap!!, editTextValue)
160+
val currentBitmap = bitmap
161+
if (currentBitmap != null) {
162+
viewModel.generate(currentBitmap, editTextValue)
149163
}
150164
},
151-
enabled = !isGenerating && pictureAvailable,
165+
enabled = uiState !is GeminiMultimodalUiState.Loading && bitmap != null,
152166
) {
153167
Icon(Icons.Default.SmartToy, contentDescription = "Robot")
154168
Text(modifier = Modifier.padding(start = 8.dp), text = "Generate")
@@ -158,14 +172,26 @@ fun GeminiMultimodalScreen(viewModel: GeminiMultimodalViewModel = hiltViewModel(
158172
.height(24.dp),
159173
)
160174

161-
if (isGenerating) {
162-
Text(
163-
text = stringResource(R.string.geminimultimodal_generating),
164-
)
165-
} else {
166-
Text(
167-
text = textResponse,
168-
)
175+
when (uiState) {
176+
is GeminiMultimodalUiState.Initial -> {
177+
Text(
178+
text = stringResource(id = R.string.geminimultimodal_generation_placeholder),
179+
style = MaterialTheme.typography.bodySmall,
180+
)
181+
}
182+
is GeminiMultimodalUiState.Loading -> {
183+
CircularProgressIndicator()
184+
}
185+
is GeminiMultimodalUiState.Success -> {
186+
Text(
187+
text = (uiState as GeminiMultimodalUiState.Success).generatedText,
188+
)
189+
}
190+
is GeminiMultimodalUiState.Error -> {
191+
Text(
192+
text = (uiState as GeminiMultimodalUiState.Error).errorMessage ?: stringResource(R.string.unknown_error),
193+
)
194+
}
169195
}
170196
}
171197
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
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+
* https://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+
package com.android.ai.samples.geminimultimodal.ui
17+
18+
sealed interface GeminiMultimodalUiState {
19+
data object Initial : GeminiMultimodalUiState
20+
data object Loading : GeminiMultimodalUiState
21+
data class Success(val generatedText: String) : GeminiMultimodalUiState
22+
data class Error(val errorMessage: String?) : GeminiMultimodalUiState
23+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
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+
* https://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+
package com.android.ai.samples.geminimultimodal.ui
17+
18+
import android.graphics.Bitmap
19+
import androidx.lifecycle.ViewModel
20+
import androidx.lifecycle.viewModelScope
21+
import com.android.ai.samples.geminimultimodal.data.GeminiDataSource
22+
import dagger.hilt.android.lifecycle.HiltViewModel
23+
import javax.inject.Inject
24+
import kotlinx.coroutines.flow.MutableStateFlow
25+
import kotlinx.coroutines.flow.StateFlow
26+
import kotlinx.coroutines.launch
27+
28+
@HiltViewModel
29+
class GeminiMultimodalViewModel @Inject constructor(private val geminiDataSource: GeminiDataSource) : ViewModel() {
30+
31+
private val _uiState = MutableStateFlow<GeminiMultimodalUiState>(GeminiMultimodalUiState.Initial)
32+
val uiState: StateFlow<GeminiMultimodalUiState> = _uiState
33+
34+
fun generate(bitmap: Bitmap, prompt: String) {
35+
_uiState.value = GeminiMultimodalUiState.Loading
36+
viewModelScope.launch {
37+
try {
38+
val result = geminiDataSource.generateText(bitmap, prompt)
39+
_uiState.value = GeminiMultimodalUiState.Success(result)
40+
} catch (e: Exception) {
41+
_uiState.value = GeminiMultimodalUiState.Error(e.message)
42+
}
43+
}
44+
}
45+
}

ai-catalog/samples/gemini-multimodal/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
<resources>
33
<string name="geminimultimodal_title_bar">Gemini Multimodal</string>
44
<string name="geminimultimodal_prompt_placeholder">Describe this picture in a funny way with a lot of emojis</string>
5+
<string name="geminimultimodal_generation_placeholder">(Take a picture and enter a prompt to start generating.)</string>
6+
<string name="geminimultimodal_take_a_picture">(Take a picture.)</string>"
57
<string name="geminimultimodal_generating">Generating&#8230;</string>
68
<string name="see_code">See code</string>
9+
<string name="unknown_error">Unknown error</string>
710
</resources>

0 commit comments

Comments
 (0)