Skip to content

Commit b3cc540

Browse files
authored
feat(ai-logic): add a SVG generator quickstart that uses Gemini 3 Flash Preview (#2746)
1 parent 2cb62f1 commit b3cc540

6 files changed

Lines changed: 244 additions & 2 deletions

File tree

firebase-ai/app/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ dependencies {
8181
implementation(platform(libs.firebase.bom))
8282
implementation(libs.firebase.ai)
8383

84+
// Image loading
85+
implementation(libs.coil3.coil.compose)
86+
implementation(libs.coil.network.okhttp)
87+
implementation(libs.coil.svg)
88+
8489
testImplementation(libs.junit)
8590
androidTestImplementation(libs.androidx.junit)
8691
androidTestImplementation(libs.androidx.espresso.core)

firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,5 +408,28 @@ val FIREBASE_AI_SAMPLES = listOf(
408408
thinkingBudget = -1 // Dynamic Thinking
409409
}
410410
}
411-
)
411+
),
412+
Sample(
413+
title = "SVG Generator",
414+
description = "Use Gemini 3 Flash preview to create SVG illustrations",
415+
navRoute = "svg",
416+
categories = listOf(Category.IMAGE, Category.TEXT),
417+
initialPrompt = content {
418+
text(
419+
"a kitten"
420+
)
421+
},
422+
generationConfig = generationConfig {
423+
thinkingConfig {
424+
thinkingBudget = -1
425+
}
426+
},
427+
systemInstructions = content { text("""
428+
You are an expert at turning image prompts into SVG code. When given a prompt,
429+
use your creativity to code a 800x600 SVG rendering of it.
430+
Always add viewBox="0 0 800 600" to the root svg tag. Do
431+
not import external assets, they won't work. Return ONLY the SVG code, nothing else,
432+
no commentary.
433+
""".trimIndent()) }
434+
),
412435
)

firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoRoute
3636
import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoScreen
3737
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenRoute
3838
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenScreen
39+
import com.google.firebase.quickstart.ai.feature.svg.SvgRoute
40+
import com.google.firebase.quickstart.ai.feature.svg.SvgScreen
3941
import com.google.firebase.quickstart.ai.feature.text.ChatRoute
4042
import com.google.firebase.quickstart.ai.feature.text.ChatScreen
4143
import com.google.firebase.quickstart.ai.feature.text.TextGenRoute
@@ -47,7 +49,6 @@ class MainActivity : ComponentActivity() {
4749
@OptIn(ExperimentalMaterial3Api::class)
4850
override fun onCreate(savedInstanceState: Bundle?) {
4951
super.onCreate(savedInstanceState)
50-
5152
enableEdgeToEdge()
5253
catImage = BitmapFactory.decodeResource(applicationContext.resources, R.drawable.cat)
5354
setContent {
@@ -98,6 +99,9 @@ class MainActivity : ComponentActivity() {
9899
"text" -> {
99100
navController.navigate(TextGenRoute(it.id))
100101
}
102+
"svg" -> {
103+
navController.navigate(SvgRoute(it.id))
104+
}
101105
}
102106
}
103107
)
@@ -125,6 +129,9 @@ class MainActivity : ComponentActivity() {
125129
composable<TextGenRoute> {
126130
TextGenScreen()
127131
}
132+
composable<SvgRoute> {
133+
SvgScreen()
134+
}
128135
}
129136
}
130137
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package com.google.firebase.quickstart.ai.feature.svg
2+
3+
import androidx.compose.foundation.layout.Box
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.fillMaxSize
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.lazy.LazyColumn
9+
import androidx.compose.foundation.lazy.items
10+
import androidx.compose.material3.Card
11+
import androidx.compose.material3.CardDefaults
12+
import androidx.compose.material3.CircularProgressIndicator
13+
import androidx.compose.material3.ElevatedCard
14+
import androidx.compose.material3.MaterialTheme
15+
import androidx.compose.material3.OutlinedTextField
16+
import androidx.compose.material3.Text
17+
import androidx.compose.material3.TextButton
18+
import androidx.compose.runtime.Composable
19+
import androidx.compose.runtime.getValue
20+
import androidx.compose.runtime.mutableStateOf
21+
import androidx.compose.runtime.saveable.rememberSaveable
22+
import androidx.compose.runtime.setValue
23+
import androidx.compose.ui.Alignment
24+
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.platform.LocalContext
26+
import androidx.compose.ui.unit.dp
27+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
28+
import androidx.lifecycle.viewmodel.compose.viewModel
29+
import coil3.compose.SubcomposeAsyncImage
30+
import coil3.request.ImageRequest
31+
import coil3.request.crossfade
32+
import coil3.svg.SvgDecoder
33+
import kotlinx.coroutines.Dispatchers
34+
import kotlinx.serialization.Serializable
35+
import java.nio.ByteBuffer
36+
37+
@Serializable
38+
class SvgRoute(val sampleId: String)
39+
40+
@Composable
41+
fun SvgScreen(
42+
svgViewModel: SvgViewModel = viewModel<SvgViewModel>()
43+
) {
44+
var prompt by rememberSaveable { mutableStateOf(svgViewModel.initialPrompt) }
45+
val errorMessage by svgViewModel.errorMessage.collectAsStateWithLifecycle()
46+
val isLoading by svgViewModel.isLoading.collectAsStateWithLifecycle()
47+
val generatedSvgs by svgViewModel.generatedSvgs.collectAsStateWithLifecycle()
48+
49+
Column {
50+
ElevatedCard(
51+
modifier = Modifier
52+
.padding(all = 16.dp)
53+
.fillMaxWidth(),
54+
shape = MaterialTheme.shapes.large
55+
) {
56+
OutlinedTextField(
57+
value = prompt,
58+
label = { Text("Generate a SVG of") },
59+
placeholder = { Text("Enter text to generate image") },
60+
onValueChange = { prompt = it },
61+
modifier = Modifier
62+
.padding(16.dp)
63+
.fillMaxWidth()
64+
)
65+
TextButton(
66+
onClick = {
67+
svgViewModel.generateSVG(prompt)
68+
},
69+
modifier = Modifier
70+
.padding(horizontal = 16.dp, vertical = 16.dp)
71+
.align(Alignment.End)
72+
) {
73+
Text("Generate")
74+
}
75+
}
76+
if (isLoading) {
77+
Box(
78+
contentAlignment = Alignment.Center,
79+
modifier = Modifier
80+
.padding(all = 8.dp)
81+
.align(Alignment.CenterHorizontally)
82+
) {
83+
CircularProgressIndicator()
84+
}
85+
}
86+
LazyColumn(
87+
modifier = Modifier.fillMaxSize()
88+
) {
89+
items(generatedSvgs) { svg ->
90+
Card(
91+
modifier = Modifier
92+
.padding(horizontal = 16.dp, vertical = 8.dp)
93+
.fillMaxWidth(),
94+
shape = MaterialTheme.shapes.large,
95+
colors = CardDefaults.cardColors(
96+
containerColor = MaterialTheme.colorScheme.onSecondaryContainer
97+
)
98+
) {
99+
SubcomposeAsyncImage(
100+
model = ImageRequest.Builder(LocalContext.current)
101+
.data(ByteBuffer.wrap(svg.toByteArray()))
102+
.decoderFactory(SvgDecoder.Factory())
103+
.decoderCoroutineContext(Dispatchers.Main)
104+
.crossfade(true)
105+
.build(),
106+
contentDescription = "Generated SVG",
107+
modifier = Modifier
108+
.fillMaxWidth()
109+
)
110+
}
111+
}
112+
}
113+
errorMessage?.let {
114+
Card(
115+
modifier = Modifier
116+
.padding(horizontal = 16.dp)
117+
.fillMaxWidth(),
118+
shape = MaterialTheme.shapes.large,
119+
colors = CardDefaults.cardColors(
120+
containerColor = MaterialTheme.colorScheme.errorContainer
121+
)
122+
) {
123+
Text(
124+
text = it,
125+
color = MaterialTheme.colorScheme.error,
126+
modifier = Modifier.padding(all = 16.dp)
127+
)
128+
}
129+
}
130+
}
131+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.google.firebase.quickstart.ai.feature.svg
2+
3+
import androidx.compose.runtime.mutableStateListOf
4+
import androidx.lifecycle.SavedStateHandle
5+
import androidx.lifecycle.ViewModel
6+
import androidx.lifecycle.viewModelScope
7+
import androidx.navigation.toRoute
8+
import com.google.firebase.Firebase
9+
import com.google.firebase.ai.GenerativeModel
10+
import com.google.firebase.ai.ai
11+
import com.google.firebase.ai.type.GenerativeBackend
12+
import com.google.firebase.ai.type.TextPart
13+
import com.google.firebase.ai.type.asTextOrNull
14+
import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES
15+
import com.google.firebase.quickstart.ai.feature.text.ChatRoute
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.flow.MutableStateFlow
18+
import kotlinx.coroutines.flow.StateFlow
19+
import kotlinx.coroutines.launch
20+
21+
class SvgViewModel(
22+
savedStateHandle: SavedStateHandle
23+
) : ViewModel() {
24+
private val sampleId = savedStateHandle.toRoute<ChatRoute>().sampleId
25+
private val sample = FIREBASE_AI_SAMPLES.first { it.id == sampleId }
26+
val initialPrompt: String =
27+
sample.initialPrompt?.parts
28+
?.filterIsInstance<TextPart>()
29+
?.first()
30+
?.asTextOrNull().orEmpty()
31+
32+
private val _isLoading = MutableStateFlow(false)
33+
val isLoading: StateFlow<Boolean> = _isLoading
34+
35+
private val _errorMessage = MutableStateFlow<String?>(null)
36+
val errorMessage: StateFlow<String?> = _errorMessage
37+
38+
private val _generatedSvgList = mutableStateListOf<String>()
39+
val generatedSvgs: StateFlow<List<String>> =
40+
MutableStateFlow<List<String>>(_generatedSvgList)
41+
42+
private val generativeModel: GenerativeModel
43+
44+
init {
45+
generativeModel = Firebase.ai(
46+
backend = sample.backend
47+
).generativeModel(
48+
modelName = sample.modelName ?: "gemini-3-flash-preview",
49+
systemInstruction = sample.systemInstructions,
50+
generationConfig = sample.generationConfig,
51+
tools = sample.tools
52+
)
53+
}
54+
55+
fun generateSVG(prompt: String) {
56+
_isLoading.value = true
57+
viewModelScope.launch(Dispatchers.IO) {
58+
try {
59+
val response = generativeModel.generateContent(prompt)
60+
response.text?.let {
61+
_generatedSvgList.add(0, it)
62+
}
63+
_errorMessage.value = null
64+
} catch (e: Exception) {
65+
_errorMessage.value = e.localizedMessage
66+
} finally {
67+
_isLoading.value = false
68+
}
69+
}
70+
71+
}
72+
}

gradle/libs.versions.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ activityCompose = "1.12.1"
33
agp = "8.13.2"
44
camerax = "1.5.2"
55
coilCompose = "2.7.0"
6+
coil3Compose = "3.3.0"
67
composeBom = "2025.12.00"
78
composeNavigation = "2.9.6"
89
coreKtx = "1.17.0"
@@ -44,6 +45,9 @@ androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
4445
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
4546
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
4647
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
48+
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil3Compose" }
49+
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3Compose" }
50+
coil3-coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3Compose" }
4751
compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation"}
4852
firebase-ai = { module = "com.google.firebase:firebase-ai" }
4953
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }

0 commit comments

Comments
 (0)