diff --git a/app/src/main/java/com/example/bmicalculator/MainActivity.kt b/app/src/main/java/com/example/bmicalculator/MainActivity.kt index b836674..21c8478 100644 --- a/app/src/main/java/com/example/bmicalculator/MainActivity.kt +++ b/app/src/main/java/com/example/bmicalculator/MainActivity.kt @@ -1,323 +1,393 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ + /* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -/* -UTS - BMI Calculator Project -Nama : Raihan Ariq Muzakki -NPM : 202310715297 -Kelas : F5A5 - */ + /* + UTS - BMI Calculator Project + Nama : Raihan Ariq Muzakki + NPM : 202310715297 + Kelas : F5A5 + */ -package com.example.bmicalculator + package com.example.bmicalculator -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.example.bmicalculator.ui.theme.BMICalculatorTheme -import java.text.DecimalFormat -import kotlin.math.pow + import android.os.Bundle + import androidx.activity.ComponentActivity + import androidx.activity.compose.setContent + import androidx.activity.enableEdgeToEdge + import androidx.annotation.DrawableRes + import androidx.annotation.StringRes + import androidx.compose.foundation.Image + import androidx.compose.foundation.layout.Arrangement + import androidx.compose.foundation.layout.Column + import androidx.compose.foundation.layout.Row + import androidx.compose.foundation.layout.Spacer + import androidx.compose.foundation.layout.fillMaxSize + import androidx.compose.foundation.layout.fillMaxWidth + import androidx.compose.foundation.layout.height + import androidx.compose.foundation.layout.padding + import androidx.compose.foundation.layout.safeDrawingPadding + import androidx.compose.foundation.layout.size + import androidx.compose.foundation.layout.statusBarsPadding + import androidx.compose.foundation.layout.width + import androidx.compose.foundation.layout.wrapContentWidth + import androidx.compose.foundation.rememberScrollState + import androidx.compose.foundation.text.KeyboardOptions + import androidx.compose.foundation.verticalScroll + import androidx.compose.material3.Button + import androidx.compose.material3.Icon + import androidx.compose.material3.MaterialTheme + import androidx.compose.material3.Surface + import androidx.compose.material3.Switch + import androidx.compose.material3.Text + import androidx.compose.material3.TextField + import androidx.compose.runtime.Composable + import androidx.compose.runtime.LaunchedEffect + import androidx.compose.runtime.getValue + import androidx.compose.runtime.mutableStateOf + import androidx.compose.runtime.remember + import androidx.compose.runtime.setValue + import androidx.compose.ui.Alignment + import androidx.compose.ui.Modifier + import androidx.compose.ui.res.painterResource + import androidx.compose.ui.res.stringResource + import androidx.compose.ui.text.font.FontWeight + import androidx.compose.ui.text.input.ImeAction + import androidx.compose.ui.text.input.KeyboardType + import androidx.compose.ui.text.style.TextAlign + import androidx.compose.ui.tooling.preview.Preview + import androidx.compose.ui.unit.dp + import com.example.bmicalculator.ui.theme.BMICalculatorTheme + import java.text.DecimalFormat + import kotlin.math.pow -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - enableEdgeToEdge() - super.onCreate(savedInstanceState) - setContent { - BMICalculatorTheme { - Surface( - modifier = Modifier.fillMaxSize(), - ) { - BMICalculatorLayout() + class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + setContent { + BMICalculatorTheme { + Surface( + modifier = Modifier.fillMaxSize(), + ) { + BMICalculatorLayout() + } } } } } -} -// Tata letak Tampilan Aplikasi -@Composable -fun BMICalculatorLayout() { - var heightInput by remember { mutableStateOf("") } - var weightInput by remember { mutableStateOf("") } - var unitUSC by remember { mutableStateOf(false) } + // Tata letak Tampilan Aplikasi + @Composable + fun BMICalculatorLayout() { + var heightInput by remember { mutableStateOf("") } + var weightInput by remember { mutableStateOf("") } + var unitUSC by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf("") } - // State baru untuk mengontrol kapan output ditampilkan - var showResult by remember { mutableStateOf(false) } + // State baru untuk mengontrol kapan output ditampilkan + var showResult by remember { mutableStateOf(false) } - // Reset nilai input ketika unit berubah - LaunchedEffect(unitUSC) { - heightInput = "" - weightInput = "" - showResult = false - } + // Reset nilai input ketika unit berubah + LaunchedEffect(unitUSC) { + heightInput = "" + weightInput = "" + showResult = false + errorMessage = "" + } - val bmiHeight = heightInput.toDoubleOrNull() ?: 0.0 - val bmiWeight = weightInput.toDoubleOrNull() ?: 0.0 - val bmi = calculateBMI(bmiHeight, bmiWeight, unitUSC) - val category = calculateBMICategory(bmi) + val bmiHeight = heightInput.toDoubleOrNull() ?: 0.0 + val bmiWeight = weightInput.toDoubleOrNull() ?: 0.0 + val bmi = calculateBMI(bmiHeight, bmiWeight, unitUSC) + val category = calculateBMICategory(bmi) - Column( - modifier = Modifier - .statusBarsPadding() - .padding(horizontal = 40.dp) - .verticalScroll(rememberScrollState()) - .safeDrawingPadding(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - - Image( - painter = painterResource(R.drawable.health_report), - contentDescription = "BMI Icon", + Column( modifier = Modifier - .padding(top = 24.dp, bottom = 16.dp) - .size(80.dp) - ) - - Text( - text = stringResource(R.string.calculate_bmi), - style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.Bold), - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 32.dp) - ) - - EditNumberField( - label = if (unitUSC) R.string.heightInch else R.string.heightCm, - leadingIcon = R.drawable.number, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Next - ), - value = heightInput, - onValueChanged = { heightInput = it }, - modifier = Modifier - .padding(bottom = 32.dp) - .fillMaxWidth() - ) - - EditNumberField( - label = if (unitUSC) R.string.weightPound else R.string.weightKg, - leadingIcon = R.drawable.number, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done - ), - value = weightInput, - onValueChanged = { weightInput = it }, - modifier = Modifier - .padding(bottom = 32.dp) - .fillMaxWidth() - ) - - UnitUSCFormulaRow( - unitUSC = unitUSC, - onUSCChanged = { unitUSC = it }, - modifier = Modifier.padding(bottom = 24.dp) - ) - - // Button Kalkulasi dan Clear Field Text - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween + .statusBarsPadding() + .padding(horizontal = 40.dp) + .verticalScroll(rememberScrollState()) + .safeDrawingPadding(), + horizontalAlignment = Alignment.CenterHorizontally ) { - Button( - onClick = { - showResult = true - }, - modifier = Modifier.weight(1f) - ) { - Text("Calculate") - } - - Spacer(modifier = Modifier.width(16.dp)) - - Button( - onClick = { - heightInput = "" - weightInput = "" - showResult = false - }, - modifier = Modifier.weight(1f) - ) { - Text("Clear") - } - } - - Spacer(modifier = Modifier.height(32.dp)) - - if (showResult) { - Text( - text = "BMI: $bmi", - style = MaterialTheme.typography.displaySmall, - textAlign = TextAlign.Center, - fontWeight = FontWeight.SemiBold, - modifier = Modifier.fillMaxWidth() + Image( + painter = painterResource(R.drawable.health_report), + contentDescription = "BMI Icon", + modifier = Modifier + .padding(top = 24.dp, bottom = 16.dp) + .size(80.dp) ) Text( - text = category, - style = MaterialTheme.typography.displayMedium, + text = stringResource(R.string.calculate_bmi), + style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.Bold), textAlign = TextAlign.Center, - fontWeight = FontWeight.ExtraBold, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 32.dp) ) - } - Spacer(modifier = Modifier.height(150.dp)) + EditNumberField( + label = if (unitUSC) R.string.heightInch else R.string.heightCm, + leadingIcon = R.drawable.number, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Next + ), + value = heightInput, + onValueChanged = { heightInput = it }, + modifier = Modifier + .padding(bottom = 32.dp) + .fillMaxWidth() + ) + + EditNumberField( + label = if (unitUSC) R.string.weightPound else R.string.weightKg, + leadingIcon = R.drawable.number, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + value = weightInput, + onValueChanged = { weightInput = it }, + modifier = Modifier + .padding(bottom = 32.dp) + .fillMaxWidth() + ) + + UnitUSCFormulaRow( + unitUSC = unitUSC, + onUSCChanged = { unitUSC = it }, + modifier = Modifier.padding(bottom = 24.dp) + ) + + // Button Kalkulasi dan Clear Field Text + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + + Button( + onClick = { + // Validasi angka + if (bmiHeight == 0.0 || bmiWeight == 0.0) { + errorMessage = "Input harus berupa angka yang valid." + showResult = false + return@Button + } + + // Validasi untuk SI Units + if (!unitUSC) { + if (bmiHeight !in 50.0..300.0 && bmiWeight !in 20.0..500.0) { + errorMessage = + "Masukkan Tinggi Badan pada rentang 50-300 cm \ndan\n Berat Badan pada rentang 20-500 kg" + showResult = false + return@Button + } + else if (bmiHeight !in 50.0..300.0) { + errorMessage = "Tinggi harus berada pada rentang 50–300 cm." + showResult = false + return@Button + } + else if (bmiWeight !in 20.0..500.0) { + errorMessage = "Berat harus berada pada rentang 20–500 kg." + showResult = false + return@Button + } + + } + + // Validasi untuk USC Units + if (unitUSC) { + if (bmiHeight !in 20.0..120.0 && bmiWeight !in 44.0..1100.0){ + errorMessage = + "Masukkan Tinggi Badan pada rentang 20-120 in dan\n Berat Badan pada rentang 44-110 kg" + showResult = false + return@Button + } + else if (bmiHeight !in 20.0..120.0) { + errorMessage = "Tinggi harus berada pada rentang 20–120 in." + showResult = false + return@Button + } + else if (bmiWeight !in 44.0..1100.0) { + errorMessage = "Berat harus berada pada rentang 44–1100 lbs." + showResult = false + return@Button + } + + } + + // Jika valid + errorMessage = "" + showResult = true + }, + modifier = Modifier.weight(1f) + ) { + Text("Calculate") + } + + Spacer(modifier = Modifier.width(16.dp)) + + Button( + onClick = { + heightInput = "" + weightInput = "" + showResult = false + errorMessage = "" + }, + modifier = Modifier.weight(1f) + ) { + Text("Clear") + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + // Pesan Error + if (errorMessage.isNotEmpty()) { + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.SemiBold, + modifier = Modifier + .padding(top = 16.dp) + .fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + // Output BMI + if (showResult) { + Text( + text = "BMI: $bmi", + style = MaterialTheme.typography.displaySmall, + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.fillMaxWidth() + ) + + Text( + text = category, + style = MaterialTheme.typography.displayMedium, + textAlign = TextAlign.Center, + fontWeight = FontWeight.ExtraBold, + modifier = Modifier.fillMaxWidth() + ) + } + + Spacer(modifier = Modifier.height(150.dp)) + } } -} -// Fungsi media Input Tinggi Badan dan Berat Badan -@Composable -fun EditNumberField( - @StringRes label: Int, - @DrawableRes leadingIcon: Int, - keyboardOptions: KeyboardOptions, - value: String, - onValueChanged: (String) -> Unit, - modifier: Modifier = Modifier -) { - TextField( - value = value, - singleLine = true, - leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) }, - modifier = modifier, - onValueChange = onValueChanged, - label = { Text(stringResource(label)) }, - keyboardOptions = keyboardOptions - ) -} - -@Composable -fun UnitUSCFormulaRow( - unitUSC: Boolean, - onUSCChanged: (Boolean) -> Unit, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + // Fungsi media Input Tinggi Badan dan Berat Badan + @Composable + fun EditNumberField( + @StringRes label: Int, + @DrawableRes leadingIcon: Int, + keyboardOptions: KeyboardOptions, + value: String, + onValueChanged: (String) -> Unit, + modifier: Modifier = Modifier ) { - Text(text = stringResource(R.string.use_usc), fontWeight = FontWeight.SemiBold) - Switch( - modifier = Modifier - .fillMaxWidth() - .wrapContentWidth(Alignment.End), - checked = unitUSC, - onCheckedChange = onUSCChanged + TextField( + value = value, + singleLine = true, + leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) }, + modifier = modifier, + onValueChange = onValueChanged, + label = { Text(stringResource(label)) }, + keyboardOptions = keyboardOptions ) } -} -/** - * Calculates the BMI - * dengan Rumus SI Metrics Unit (Default) - * dan - * dengan Rumus USC Units - * - * Catatan: Unit Testing Sudah ada di src/test/java/calculateBMITest.kt - */ -fun calculateBMI(bmiHeight: Double, bmiWeight: Double, unitUSC: Boolean): String { - if (bmiHeight <= 0 || bmiWeight <= 0){ - return "0.0" + @Composable + fun UnitUSCFormulaRow( + unitUSC: Boolean, + onUSCChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier + ) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = stringResource(R.string.use_usc), fontWeight = FontWeight.SemiBold) + Switch( + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.End), + checked = unitUSC, + onCheckedChange = onUSCChanged + ) + } } - val heightInMeter = bmiHeight/100 // konversi centimeter ke meter + /** + * Calculates the BMI + * dengan Rumus SI Metrics Unit (Default) + * dan + * dengan Rumus USC Units + * + * Catatan: Unit Testing Sudah ada di src/test/java/calculateBMITest.kt + */ + fun calculateBMI(bmiHeight: Double, bmiWeight: Double, unitUSC: Boolean): String { + if (bmiHeight <= 0 || bmiWeight <= 0){ + return "0.0" + } - var bmi = bmiWeight / heightInMeter.pow(2) - if (unitUSC) { - bmi = 703 * (bmiWeight / bmiHeight.pow(2)) + val heightInMeter = bmiHeight/100 // konversi centimeter ke meter + + var bmi = bmiWeight / heightInMeter.pow(2) + if (unitUSC) { + bmi = 703 * (bmiWeight / bmiHeight.pow(2)) + } + + val df = DecimalFormat("#.#") + return df.format(bmi) } - val df = DecimalFormat("#.#") - return df.format(bmi) -} + /** + * Calculates the BMI Category + * bmi < 18.5 -> Underweight + * 18.5 <= bmi < 25 -> Normal + * 25 <= bmi <= 30 -> Normal + * bmi > 30 -> Overweight + * + * Catatan: Unit Testing Sudah ada di src/test/java/calculateBMITest.kt + * + */ + fun calculateBMICategory(bmi: String): String { + val bmiValue = bmi.replace(",", ".").toDoubleOrNull() ?: return "" -/** - * Calculates the BMI Category - * bmi < 18.5 -> Underweight - * 18.5 <= bmi < 25 -> Normal - * 25 <= bmi <= 30 -> Normal - * bmi > 30 -> Overweight - * - * Catatan: Unit Testing Sudah ada di src/test/java/calculateBMITest.kt - * - */ -fun calculateBMICategory(bmi: String): String { - val bmiValue = bmi.replace(",", ".").toDoubleOrNull() ?: return "" - - return when { - bmiValue == 0.0 -> "" - bmiValue < 18.5 -> "⚠️ Underweight" - bmiValue < 25.0 -> "✅ Normal" - bmiValue < 30.0 -> "⚠️ Overweight" - else -> "‼️ Obesity" + return when { + bmiValue == 0.0 -> "" + bmiValue < 18.5 -> "⚠️ Underweight" + bmiValue < 25.0 -> "✅ Normal" + bmiValue < 30.0 -> "⚠️ Overweight" + else -> "‼️ Obesity" + } } -} -@Preview(showBackground = true) -@Composable -fun BMICalculatorLayoutPreview() { - BMICalculatorTheme { - BMICalculatorLayout() + @Preview(showBackground = true) + @Composable + fun BMICalculatorLayoutPreview() { + BMICalculatorTheme { + BMICalculatorLayout() + } } -} diff --git a/app/src/main/java/com/example/bmicalculator/ui/theme/Color.kt b/app/src/main/java/com/example/bmicalculator/ui/theme/Color.kt index 56c06ea..b84b67e 100644 --- a/app/src/main/java/com/example/bmicalculator/ui/theme/Color.kt +++ b/app/src/main/java/com/example/bmicalculator/ui/theme/Color.kt @@ -17,8 +17,8 @@ package com.example.bmicalculator.ui.theme import androidx.compose.ui.graphics.Color -val md_theme_light_primary = Color(0xFFE3E2DE) -val md_theme_light_onPrimary = Color(0xFF1351AA) +val md_theme_light_primary = Color(0xFF1351AA) // Button +val md_theme_light_onPrimary = Color(0xFFE3E2DE) val md_theme_light_primaryContainer = Color(0xFFE3E2DE) val md_theme_light_onPrimaryContainer = Color(0xFF1351AA) val md_theme_light_secondary = Color(0xFFE3E2DE)