iTranslated by AI
Building a Low-Cost AI Fitness App Using Gemini 2.5 Flash-Lite, Go (Gin), and GCP
1. Introduction
In individual development, the biggest enemies are "running costs" and "maintaining motivation."
This time, I released a fitness app called BuddyLift, which "wholeheartedly affirms the user's efforts."
The most significant feature of this app is that even if it's just a 10-second workout, the AI (named Bal-kun) will give you overwhelming praise.
In this article, I will share the low-cost and robust AI implementation using Gemini 2.5 Flash-Lite, along with a serverless architecture that minimizes operational costs to the extreme.
2. System Architecture: Pursuing $0/month by Combining Managed Services
Here is the architecture I built with the obsession that "I don't want to pay a single cent for time when the app isn't being used."
User Management Implemented with Firestore
I adopted Cloud Firestore for storing user data. By optimizing the number of read and write operations, the configuration is designed to easily stay within the free tier at an individual development scale.
Go (Gin) × Cloud Run
Leveraging Go's lightweight binaries, I minimized Cloud Run's cold starts. During times without requests, the instances scale to zero, completely stopping any charges.
3. "Type-Safe" Prompt Strategy with Gemini 2.5 Flash-Lite
When handling AI responses in an app, the scariest thing is inconsistency in the output format. By utilizing ResponseSchema from the Google AI SDK for Go, I enforced types on the API side.
Enforcing Responses through Schema Definition
Instead of relying solely on the prompt, I defined the structure at the code level.
model.ResponseMIMEType = "application/json"
model.ResponseSchema = &genai.Schema{
Type: genai.TypeObject,
Properties: map[string]*genai.Schema{
"suggested_action": {
Type: genai.TypeString,
Description: "Only the specific name of the training exercise (e.g., Squat, Plank).",
},
"advice": {
Type: genai.TypeString,
Description: "Explain why this exercise was chosen based on the history, within 30 characters.",
},
"method": {
Type: genai.TypeString,
Description: "Explain the correct way to perform the exercise in about 3 steps.",
},
},
Required: []string{"suggested_action", "advice", "method"},
}
Even for 10 seconds of exercise data, "Bal-kun" returns optimal advice and explosive praise following this schema.

4. Android Implementation: Capturing with a "Stealth Layer" of Infinite Height
The UI is built with Jetpack Compose. For SNS sharing, I established a mechanism to capture long screens that extend beyond the scroll in one go.
Challenge: Capturing the display UI as-is leads to clipping
With standard capturing, you can only record the area currently visible on the screen. On the other hand, it is inefficient to rebuild the UI just for the sake of capturing it.
Solution: Virtual Rendering with GraphicsLayer and layout
I adopted a method of overlaying a dedicated layer that is "invisible to the user but internally rendered at full size."
val graphicsLayer = rememberGraphicsLayer()
Box(modifier = Modifier.fillMaxSize()) {
// --- 1. Capture-only layer (Never visible to the user) ---
Box(
modifier = Modifier
.layout { measurable, constraints ->
// ★ Point: Measure with "infinite" height constraints
val infiniteHeightConstraints = constraints.copy(
minHeight = 0,
maxHeight = Constraints.Infinity
)
val placeable = measurable.measure(infiniteHeightConstraints)
// Report 0,0 to the parent element (Box) so it doesn't take up space on the screen
layout(0, 0) {
placeable.place(0, 0)
}
}
.drawWithCache {
onDrawWithContent {
// Record content to graphicsLayer
graphicsLayer.record {
this@onDrawWithContent.drawContent()
}
// Do not draw to the actual screen (Canvas)
}
}
) {
Column(modifier = Modifier.width(360.dp).background(backgroundBrush)) {
// Full content without scrolling
SuccessContent(streakCount, message)
}
}
// --- 2. Display layer (Scrollable screen for user interaction) ---
Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
SuccessContent(streakCount, message)
}
}
Through this implementation—which is almost a hack, where "measurement is infinite but reporting is zero"—it became possible to instantly generate an "ultra-long image with the exact same design as the screen currently being viewed."
5. Conclusion
As the culmination of the architecture introduced here (Go/Gin, Gemini 2.5, Firestore, Compose), I have released the fitness app BuddyLift.
This app is the result of trial and error to maximize user experience while keeping costs low. I would be happy if this article provides some hints for your own individual development, and as a developer, nothing would bring me more joy than having you actually try the app and provide feedback.

BuddyLift 〜 Training with Bal-kun 〜
Discussion