Skip to main content

How to Make an BMI Calculator App

Copied to clipboard!
No boilerplate memorization required

How to Vibe Code a Native BMI Calculator App

Stay in your creative flow. Direct AI models with high-level intent, stitch together Jetpack Compose UI, and ship an installed BMI & wellness tracker APK on your phone in under 25 minutes.

Core Stack

Kotlin Compose

Persistence

DataStore / Room

AI Partner

Claude / Gemini

Target Time

15โ€“25 Mins

The Philosophy

The Vibe Coding Mobile Philosophy

Traditional mobile health app development has an intimidating reputation: XML layouts, manual unit conversion listeners, division-by-zero edge cases, and boilerplate SQLite helpers. Vibe coding turns this upside down. You operate as the Product Director & Architect: defining the visual feel, interactive sliders, and WHO category gauge while conversational AI synthesizes the single-file Compose code and local storage wiring.

Old-School Mobile Stress

  • Manually wiring XML input fields and matching IDs with findViewById.
  • Debugging Metric (kg/mยฒ) vs. Imperial (703 ร— lb/inยฒ) conversion bugs by hand.
  • Writing boilerplate SQLite open helpers just to log weekly weight entries.
  • Wrestling custom Canvas trigonometry for hours to draw a category gauge.

The Vibe Coder Way

  • Modern Jetpack Compose (pure Kotlin, reactive sliders, instant live calculation).
  • Describe the UI vibe: "Clean wellness card with smooth height/weight sliders and an animated WHO spectrum bar."
  • Feed compiler errors back into AI: "Fix this missing import without altering my BMI math."
  • Test immediately on your real phone with Wireless Debugging.
Execution

The 4-Step Blueprint

From a blank Android Studio canvas to logging real health check-ins on your handset.

Step 01

Spawning the Empty Compose Canvas

Open Android Studio and select New Project > Empty Activity (with Compose logo). Name it VibeBMI.

๐Ÿ’ก Pro Vibe Tip: Do not pick the legacy XML Views template. Jetpack Compose keeps sliders, unit toggles, and reactive BMI formulas inside one clean Kotlin file that LLMs generate flawlessly.
Step 02

The "One-Shot Architect" Prompt

Give your LLM the full mental model upfront: Metric/Imperial switcher, smooth height & weight sliders, live BMI calculation, and WHO category interpretation.

๐Ÿ”‘ Key Constraint: Tell the AI: "Provide everything needed in a single MainActivity.kt so I can copy-paste and verify immediately."
Step 03

Styling & Health Micro-Interactions

Inject polish. Add a color-coded WHO spectrum bar (Underweight, Normal, Overweight, Obese), healthy target weight range hints, and haptic feedback as sliders move.

โœจ Iteration Secret: Use Android Studio's @Preview or Live Edit to tweak slider bounds and badge colors without a full rebuild.
Step 04

Run on Your Real Phone

Turn on Developer Options > Wireless Debugging on your Android phone. Pair via QR code in Android Studio and hit Run (Shift+F10).

๐Ÿ† The Grand Payoff: You get a fast, private, zero-ad BMI & weight tracker on your personal phone built entirely through natural language.
Interactive Tool

Vibe Prompt Configurator

Select your desired aesthetics and health features. Copy the generated prompt directly into Claude, Cursor, or Gemini.

Special Feature Vibe
Optional custom details added straight into your master prompt.
Generated Master Prompt
Ready to paste into Claude, Gemini, or ChatGPT Single-file Compose ready
Product Spec

Essential Elements of a BMI Calculator App

Body Mass Index (BMI) uses height and weight to estimate body fat and screen for health risks. Make sure your app covers these core building blocks:

  • Height & Weight Input Controls

    Allow users to smoothly enter or slide their height and weight with instant toggling between Metric (cm/kg) and Imperial (ft-in/lbs) units.

  • Real-Time BMI Calculation

    Compute BMI accurately on the fly (weight / heightยฒ) with safe minimum bounds so zero or incomplete inputs never crash the app.

  • WHO Standard Interpretation

    Classify results clearly according to World Health Organization thresholds: Underweight (<18.5), Normal (18.5โ€“24.9), Overweight (25โ€“29.9), and Obese (30+).

  • Customized Feedback & Target Range

    Provide encouraging, plain-language guidance based on the user's score alongside the healthy weight range for their specific height.

  • Progress & Companion Health Tools

    Let users log check-ins to track weight over time, and optionally extend the app with a daily calorie (BMR/TDEE) calculator or workout tracker.

  • Clean, Uncluttered UI & UX

    Avoid overwhelming medical jargon, guide users step-by-step from input to result, and keep high contrast on numbers and category badges.

UX Checklist Before Shipping: Keep the main screen focused on a single primary flow (input → score → interpretation), use clear everyday language, and test with real users on a physical phone to verify slider comfort.
Starter Template

The "Golden" Single-File Compose Template

Replace the contents of your MainActivity.kt with this to run a working BMI Calculator right now.

app/src/main/java/.../MainActivity.kt
Kotlin + Compose
package com.example.vibebmi

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.util.Locale
import kotlin.math.pow

// --- 1. Data Model & WHO Classification ---
data class BmiLog(
    val id: Long = System.currentTimeMillis(),
    val bmi: Float,
    val category: String,
    val summary: String
)

enum class BmiCategory(val label: String, val color: Color, val advice: String) {
    UNDERWEIGHT("Underweight", Color(0xFF38BDF8), "Focus on nutrient-dense meals and strength training."),
    NORMAL("Normal Weight", Color(0xFF10B981), "Great balance! Keep up your active lifestyle."),
    OVERWEIGHT("Overweight", Color(0xFFF59E0B), "A modest calorie deficit and daily walks help."),
    OBESE("Obese", Color(0xFFF43F5E), "Consider consulting a healthcare provider for a tailored plan.")
}

fun classifyBmi(bmi: Float): BmiCategory = when {
    bmi < 18.5f -> BmiCategory.UNDERWEIGHT
    bmi < 25.0f -> BmiCategory.NORMAL
    bmi < 30.0f -> BmiCategory.OVERWEIGHT
    else -> BmiCategory.OBESE
}

// --- 2. Main Entry Point ---
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme(
                colorScheme = darkColorScheme(
                    background = Color(0xFF0D141C),
                    surface = Color(0xFF16202B),
                    primary = Color(0xFF10B981)
                )
            ) {
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    VibeBmiScreen()
                }
            }
        }
    }
}

// --- 3. Interactive Vibe BMI Screen ---
@Composable
fun VibeBmiScreen() {
    var isMetric by remember { mutableStateOf(true) }
    var heightCm by remember { mutableFloatStateOf(172f) }
    var weightKg by remember { mutableFloatStateOf(68f) }
    var logs by remember { mutableStateOf(listOf<BmiLog>()) }

    val heightM = (heightCm / 100f).coerceAtLeast(0.5f)
    val bmi = weightKg / heightM.pow(2)
    val category = classifyBmi(bmi)
    val badgeColor by animateColorAsState(targetValue = category.color, label = "bmiColor")

    val minHealthyKg = 18.5f * heightM.pow(2)
    val maxHealthyKg = 24.9f * heightM.pow(2)

    Scaffold(
        topBar = {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .statusBarsPadding()
                    .padding(horizontal = 20.dp, vertical = 16.dp),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Column {
                    Text("Vibe BMI โšก", fontSize = 26.sp, fontWeight = FontWeight.ExtraBold, color = Color.White)
                    Text("WHO Standard Index & Target Range", fontSize = 12.sp, color = Color(0xFF94A3B8))
                }
                FilterChip(
                    selected = isMetric,
                    onClick = { isMetric = !isMetric },
                    label = { Text(if (isMetric) "Metric (kg/cm)" else "Imperial (lb/in)") }
                )
            }
        },
        containerColor = Color.Transparent
    ) { innerPadding ->
        Column(
            modifier = Modifier
                .padding(innerPadding)
                .fillMaxSize()
                .padding(horizontal = 20.dp),
            verticalArrangement = Arrangement.spacedBy(14.dp)
        ) {
            // Live Result Card
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .clip(RoundedCornerShape(20.dp))
                    .background(Color(0xFF16202B))
                    .padding(20.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text("YOUR BODY MASS INDEX", fontSize = 11.sp, color = Color(0xFF94A3B8), fontWeight = FontWeight.Bold)
                Text(
                    text = String.format(Locale.US, "%.1f", bmi),
                    fontSize = 48.sp,
                    fontWeight = FontWeight.Black,
                    color = badgeColor
                )
                Box(
                    modifier = Modifier
                        .clip(CircleShape)
                        .background(badgeColor.copy(alpha = 0.18f))
                        .padding(horizontal = 14.dp, vertical = 4.dp)
                ) {
                    Text(category.label, color = badgeColor, fontWeight = FontWeight.Bold, fontSize = 13.sp)
                }
                Spacer(modifier = Modifier.height(8.dp))
                Text(category.advice, fontSize = 12.sp, color = Color(0xFFCBD5E1))
                Spacer(modifier = Modifier.height(6.dp))
                Text(
                    text = if (isMetric) {
                        String.format(Locale.US, "Healthy range: %.1f โ€“ %.1f kg", minHealthyKg, maxHealthyKg)
                    } else {
                        String.format(Locale.US, "Healthy range: %.0f โ€“ %.0f lbs", minHealthyKg * 2.20462f, maxHealthyKg * 2.20462f)
                    },
                    fontSize = 12.sp,
                    color = Color(0xFF10B981),
                    fontWeight = FontWeight.SemiBold
                )
            }

            // Height Slider Card
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .clip(RoundedCornerShape(16.dp))
                    .background(Color(0xFF16202B))
                    .padding(16.dp)
            ) {
                val heightLabel = if (isMetric) {
                    "${heightCm.toInt()} cm"
                } else {
                    val totalInches = (heightCm / 2.54f).toInt()
                    "${totalInches / 12}' ${totalInches % 12}\""
                }
                Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                    Text("Height", color = Color(0xFF94A3B8), fontSize = 14.sp)
                    Text(heightLabel, color = Color.White, fontWeight = FontWeight.Bold, fontSize = 16.sp)
                }
                Slider(
                    value = heightCm,
                    onValueChange = { heightCm = it },
                    valueRange = 120f..220f
                )
            }

            // Weight Slider Card
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .clip(RoundedCornerShape(16.dp))
                    .background(Color(0xFF16202B))
                    .padding(16.dp)
            ) {
                val weightLabel = if (isMetric) {
                    String.format(Locale.US, "%.1f kg", weightKg)
                } else {
                    String.format(Locale.US, "%.0f lbs", weightKg * 2.20462f)
                }
                Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                    Text("Weight", color = Color(0xFF94A3B8), fontSize = 14.sp)
                    Text(weightLabel, color = Color.White, fontWeight = FontWeight.Bold, fontSize = 16.sp)
                }
                Slider(
                    value = weightKg,
                    onValueChange = { weightKg = it },
                    valueRange = 35f..160f
                )
            }

            Button(
                onClick = {
                    val entry = BmiLog(
                        bmi = bmi,
                        category = category.label,
                        summary = "${heightCm.toInt()} cm โ€ข ${weightKg.toInt()} kg"
                    )
                    logs = listOf(entry) + logs
                },
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(14.dp)
            ) {
                Text("Save Check-In to History", color = Color.Black, fontWeight = FontWeight.Bold)
            }

            // Recent Logs
            LazyColumn(
                modifier = Modifier.weight(1f),
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(logs, key = { it.id }) { log ->
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .clip(RoundedCornerShape(12.dp))
                            .background(Color(0xFF16202B))
                            .padding(12.dp),
                        horizontalArrangement = Arrangement.SpaceBetween,
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Column {
                            Text(log.category, color = Color.White, fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
                            Text(log.summary, color = Color(0xFF94A3B8), fontSize = 12.sp)
                        }
                        Text(
                            text = String.format(Locale.US, "BMI %.1f", log.bmi),
                            color = Color(0xFF10B981),
                            fontWeight = FontWeight.Bold
                        )
                    }
                }
            }
        }
    }
}
Troubleshooting

How to Debug Without Losing Your Vibe

When Gradle or Compose flags an unresolved reference or unit conversion quirk, use these three vibe debugging rituals:

The Error Dump Rule

Copy the exact red text from the Android Studio Build tab and prompt: "Here is my Gradle compiler error. Tell me only which dependencies or imports are missing."

Single Source of Truth Math

Avoid slider drift when toggling Metric and Imperial units by instructing the AI: "Store height in cm and weight in kg internally, and only convert for display labels when Imperial is active."

Instant Wireless Run

Emulators eat heavy RAM. Pair your physical phone via Wi-Fi once, and hit Apply Changes whenever you tweak WHO category colors or slider ranges.

Extensions

The Vibe Prompt Vault

Copy-paste prompts to level up your BMI Calculator with gauges, charts, and body composition metrics.

๐ŸŽฏ Prompt: Add an Animated Semicircle Canvas Gauge for WHO Categories
Prompt Template

"I have a working Jetpack Compose BMI Calculator in MainActivity.kt. Add a custom Compose Canvas semicircle gauge above the BMI score with 4 colored arc segments (Underweight <18.5, Normal 18.5โ€“24.9, Overweight 25โ€“29.9, Obese 30+) and an animated needle that smoothly rotates to the current BMI value using animateFloatAsState."

๐Ÿ—„๏ธ Prompt: Upgrade to Persistent Weight & BMI History with Room SQLite
Prompt Template

"Refactor my BMI Calculator app to store check-ins in a local Room database. 1) Give me the exact lines to add to build.gradle.kts for Room & KSP. 2) Provide the BmiEntryEntity, BmiDao, AppDatabase, and a ViewModel collecting a Flow of entries sorted newest-first, showing the weight difference (+/- kg) since the previous entry."

๐Ÿ“ Prompt: Add Waist-to-Height Ratio & Body Fat Percentage Estimator
Prompt Template

"Extend my Jetpack Compose BMI app with an optional 'Advanced Metrics' expandable card that lets the user enter Age, Waist circumference, and Neck circumference to calculate Waist-to-Height Ratio (WHtR) and estimated Body Fat % alongside standard BMI, using clean Material 3 chips and cards."

We use cookies for ads and analytics.