Open In App

Android Jetpack Compose – Build a BMI Calculator App from Scratch

Last Updated : 14 Apr, 2023
Improve
Improve
Like Article
Like
Save
Share
Report

Body Mass Index (BMI) is a widely used measure of body fat based on height and weight. With the rise of health consciousness, BMI calculators have become popular tools to monitor and manage one’s weight and health. In this article, we will explore how to develop a BMI Calculator app for Android using Jetpack Compose and Material 3.

  • Jetpack Compose is a modern toolkit for building Android UIs declaratively
  • Material 3 is the latest version of Google’s design system for Android apps. 

We will walk through the process of building a BMI Calculator app that allows users to input their height and weight in either metric or imperial units and get their BMI and corresponding health status instantly. We will cover topics such as Composable UIs, State management, and Material 3 components. A sample video is given below to get an idea about what we are going to do in this article.

Set up your development environment

Before you begin building your app, make sure you have the latest version of Android Studio installed, and create a new jetpack compose material3 project. Add the following dependencies in the project’s app/build.gradle file.

dependencies {

    implementation platform("androidx.compose:compose-bom:2023.03.00")

    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.ui:ui-tooling-preview"
    implementation "androidx.compose.material3:material3:1.1.0-beta01"
    implementation "androidx.compose.foundation:foundation"
    implementation "androidx.activity:activity-compose:1.7.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"

    implementation "com.google.accompanist:accompanist-systemuicontroller:0.38.0"

    implementation "androidx.core:core-ktx:1.9.0"
    implementation "androidx.annotation:annotation:1.6.0"

    // Other dependencies
}

Complete build.gradle file is given below.

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'srimani7.apps.bmi.calculator'
    compileSdk 33

    defaultConfig {
        applicationId "srimani7.apps.bmi.calculator"
        minSdk 21
        targetSdk 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion project.compose_compiler_version
    }
    packagingOptions {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}

dependencies {

    implementation platform("androidx.compose:compose-bom:$project.compose_bom_version")

    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.ui:ui-tooling-preview"
    implementation 'androidx.compose.material3:material3:1.1.0-beta01'
    implementation "androidx.compose.foundation:foundation"
    implementation "androidx.activity:activity-compose:$project.activity_version"
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$project.lifecycle_version"
    implementation "androidx.navigation:navigation-compose:$project.navigation_version"

    implementation "com.google.accompanist:accompanist-systemuicontroller:$project.accompanist_version"

    implementation "androidx.core:core-ktx:$project.androidx_core_version"
    implementation "androidx.annotation:annotation:$project.androidx_annotation_version"

    testImplementation "junit:junit:$junit_version"
    androidTestImplementation "androidx.test.ext:junit:$androidx_junit_version"
    androidTestImplementation "androidx.test.espresso:espresso-core:$androidx_expresso_version"

    debugImplementation "androidx.compose.ui:ui-tooling"
    debugImplementation "androidx.compose.ui:ui-test-manifest"
}

Business Logic – BmiViewModel

Before going to design the UI we first define state and business logic. The BmiViewModel class is an important part of the BMI Calculator app, as it handles the business logic and state management of the app, and is crucial to providing a responsive and intuitive user experience. follow the below diagram for a better understanding.

Business Logic - BmiViewModel

bmi calculator – UI state & events

BmiViewModel.kt

Kotlin




class BmiViewModel : ViewModel() {
    var bmi by mutableStateOf(0.0)
        private set
    var message by mutableStateOf("")
        private set
    var selectedMode by mutableStateOf(Mode.Metric)
        private set
    var heightState by mutableStateOf(ValueState("Height", "m"))
        private set
    var weightState by mutableStateOf(ValueState("Weight", "kg"))
        private set
 
    fun updateHeight(it: String) {
        heightState = heightState.copy(value = it, error = null)
    }
 
    fun updateWeight(it: String) {
        weightState = weightState.copy(value = it, error = null)
    }
 
    fun calculate() {
        val height = heightState.toNumber()
        val weight = weightState.toNumber()
        if (height == null)
            heightState = heightState.copy(error = "Invalid number")
        else if (weight == null)
            weightState = weightState.copy(error = "Invalid number")
        else calculateBMI(height, weight, selectedMode == Mode.Metric)
    }
 
    private fun calculateBMI(height: Double, weight: Double, isMetric: Boolean = true) {
        bmi = if (isMetric)
            weight / (height * height)
        else (703 * weight) / (height * height)
 
        message = when {
            bmi < 18.5 -> "Underweight"
            bmi in 18.5..24.9 -> "Normal"
            bmi in 25.0..29.9 -> "Overweight"
            bmi >= 30.0 -> "Obsess"
            else -> error("Invalid params")
        }
    }
 
    fun updateMode(it: Mode) {
        selectedMode = it
        when (selectedMode) {
            Mode.Imperial -> {
                heightState = heightState.copy(prefix = "inch")
                weightState = weightState.copy(prefix = "pound")
            }
            Mode.Metric -> {
                heightState = heightState.copy(prefix = "m")
                weightState = weightState.copy(prefix = "kg")
            }
        }
    }
 
    fun clear() {
        heightState = heightState.copy(value = "", error = null)
        weightState = weightState.copy(value = "", error = null)
        bmi = 0.0
        message = ""
    }
 
    enum class Mode { Imperial, Metric }
}


ValueState.kt

ValueState data class is used to handle the user’s input for both height and weight.

Kotlin




data class ValueState(
    val label: String, // lable
    val suffix: String, // units
    val value: String = "", // A string representing the user's input value.
    val error: String? = null // error meaage if any
) {
    fun toNumber() = value.toDoubleOrNull()
}


Designing UI

Theme.kt

The BmiTheme composable is responsible for setting up the theme for the BMI calculator app. It also takes in a dynamicColor flag to enable a dynamic color scheme on devices running Android 12 or later. The composable uses rememberSystemUiController to set the system bar color to transparent and adjust the icon colors based on the useDarkTheme flag. Finally, it applies the MaterialTheme to wrap around the app content.

Kotlin




@Composable
fun BmiTheme(
    useDarkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable() () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (useDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        useDarkTheme -> DarkColors
        else -> LightColors
    }
    val systemUiController = rememberSystemUiController()
 
    DisposableEffect(systemUiController, useDarkTheme) {
        systemUiController.setSystemBarsColor(
            color = Color.Transparent,
            darkIcons = !useDarkTheme
        )
        onDispose {}
    }
 
    MaterialTheme(
        colorScheme = colorScheme,
        content = content
    )
}


UI Components

The following composable functions were used to build the user interface of the BMI Calculator app:

  • ModeSelector(selectedMode: BmiViewModel.Mode, updateMode: (BmiViewModel.Mode) -> Unit): A row of elevated filter chips that allows the user to switch between metric and imperial units.
  • RowScope.ActionButton(text: String, onClick: () -> Unit): A button that triggers an action when clicked.
  • CustomTextField(state: ValueState, imeAction: ImeAction, onValueChange: (String) -> Unit): A custom text field that accepts a ValueState object, which contains the current value, label, and error message of the text field. It also allows the specification of the ImeAction (such as “done” or “next”) for the keyboard, and a callback function to be triggered whenever the text in the field changes.

Kotlin




@Composable
private fun ModeSelector(selectedMode: BmiViewModel.Mode, updateMode: (BmiViewModel.Mode) -> Unit) {
    Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
        BmiViewModel.Mode.values().forEach {
            ElevatedFilterChip(selectedMode == it, onClick = { updateMode(it) },
                label = {
                    Text(it.name)
                }
            )
        }
    }
}
 
@Composable
fun RowScope.ActionButton(text: String, onClick: () -> Unit) {
    val focusManager = LocalFocusManager.current
    Button(
        onClick = { focusManager.clearFocus(); onClick() },
        shape = RoundedCornerShape(8.dp),
        modifier = Modifier.weight(1f),
        contentPadding = PaddingValues(14.dp)
    ) {
        Text(text, fontSize = 15.sp)
    }
}
 
@Composable
fun CustomTextField(state: ValueState, imeAction: ImeAction, onValueChange: (String) -> Unit, ) {
    val focusManager = LocalFocusManager.current
    OutlinedTextField(
        value = state.value,
        isError = state.error != null,
        supportingText = { state.error?.let { Text(it) } },
        label = { Text(state.label) },
        suffix = { Text(state.prefix) },
        onValueChange = onValueChange,
        singleLine = true,
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = imeAction),
        keyboardActions = KeyboardActions(onDone = {
            focusManager.clearFocus()
        })
    )
}


The main app composable

The App composable is the main UI component of the BMI Calculator app. It is composed of a Scaffold that provides the basic structure for the app, including the TopAppBar, which displays the app title. The Column component is used to lay out the different UI elements in a vertical arrangement. The UI structure consists of a top app bar with the title “BMI Calculator”. Below the app bar, there is a column that displays the calculated BMI value and its corresponding message, both of which are dynamically updated based on user input. The BMI value and message are separated by a horizontal divider.

Below the BMI value and message column, there is another column that contains the user input fields, which include two custom text fields for the user to enter their height and weight and a mode selector that lets the user choose between metric and imperial units. The mode selector is implemented using elevated filter chips. At the bottom of the UI, there is a row of two buttons: “Clear” and “Calculate”. These buttons are aligned horizontally and have equal widths.

Kotlin




Copy code
@Composable
fun App(viewModel: BmiViewModel = viewModel()) {
    Scaffold(
        topBar = {
            CenterAlignedTopAppBar(
                title = {
                    Text("BMI Calculator")
                })
            )
        }
    ) { paddingValues ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.SpaceAround
        ) {
            Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.SpaceAround
        ) {
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                Text(
                    text = "%.2f".format(viewModel.bmi),
                    style = MaterialTheme.typography.headlineSmall,
                )
                Divider(modifier = Modifier.fillMaxWidth(.7f), thickness = 2.5.dp)
                Text(text = viewModel.message,
                style = MaterialTheme.typography.bodyLarge,
                color = MaterialTheme.colorScheme.secondary)
            }
 
            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                ModeSelector(viewModel.selectedMode, updateMode = viewModel::updateMode)
                CustomTextField(viewModel.heightState,ImeAction.Next, viewModel::updateHeight)
                CustomTextField(viewModel.weightState,ImeAction.Done, viewModel::updateWeight)
            }
            Spacer(modifier = Modifier)
 
            Row(
                horizontalArrangement = Arrangement.spacedBy(8.dp),
                modifier = Modifier
                    .padding(16.dp, 12.dp)
                    .fillMaxWidth(),
            ) {
                ActionButton(text = "Clear", viewModel::clear)
                ActionButton(text = "Calculate", viewModel::calculate)
            }
        }
    }
}


MainActivity.kt

The entry point to the application. In the onCreate method, the app’s content view is set to the App composable function wrapped in a BmiTheme ( extended material 3 themes).

Kotlin




class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BmiTheme {
                App()
            }
        }
        WindowCompat.setDecorFitsSystemWindows(window, false)
    }
}


Output

Conclusion

In conclusion, building a BMI Calculator app using Compose and Material 3 is a straightforward process that allows you to create a modern and responsive user interface with minimal boilerplate code. Compose’s declarative approach to building UIs and Material 3’s ready-to-use components makes it easy to develop and maintain high-quality Android apps that meet the latest design standards. We hope this article has provided you with a useful introduction to building Android apps using Compose and Material 3.



Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads