Melakukan perubahan pada rumus perhitungan agar lebih akurat pada perhitungan usc atau non usc unit, dan sedikit perapihan pada tampilan

This commit is contained in:
dend 2025-11-06 20:48:40 +07:00
parent 099c35f19a
commit 53ba1398ff

View File

@ -1,214 +1,314 @@
/*
* 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.
*/
package com.example.tiptime
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.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.statusBarsPadding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.tiptime.ui.theme.TipTimeTheme
import java.text.NumberFormat
/**
* BMI Calculator Application
*
* NPM: [202310715051]
* Nama: [Dendi Yogia Pratama]
*/
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TipTimeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
TipTimeLayout()
BMICalculatorLayout()
}
}
}
}
}
/**
* Data class untuk menyimpan hasil kalkulasi BMI
*/
data class BMIResult(
val bmiValue: Double,
val category: String,
val isValid: Boolean = true,
val errorMessage: String = ""
)
/**
* Composable untuk switch pemilihan unit sistem (USC/SI)
*/
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
var tipInput by remember { mutableStateOf("") }
var roundUp by remember { mutableStateOf(false) }
val BmiHeight = amountInput.toDoubleOrNull() ?: 0.0
val BmiWeight = tipInput.toDoubleOrNull() ?: 0.0
val bmi = calculateBMI(BmiHeight, BmiWeight, roundUp)
val category = calculateBMICategory(BmiHeight, BmiWeight, roundUp)
Column(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.calculate_tip),
modifier = Modifier
.padding(bottom = 16.dp, top = 40.dp)
.align(alignment = Alignment.Start)
)
EditNumberField(
label = R.string.height,
leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
),
value = amountInput,
onValueChanged = { amountInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(),
)
EditNumberField(
label = R.string.weight,
leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
value = tipInput,
onValueChanged = { tipInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(),
)
RoundTheTipRow(
roundUp = roundUp,
onRoundUpChanged = { roundUp = it },
modifier = Modifier.padding(bottom = 32.dp)
)
Text(
text = stringResource(R.string.bmi_calculation, bmi),
style = MaterialTheme.typography.displaySmall
)
Text(
text = stringResource(R.string.bmi_category, category),
style = MaterialTheme.typography.displaySmall
)
Spacer(modifier = Modifier.height(150.dp))
}
}
@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 RoundTheTipRow(
roundUp: Boolean,
onRoundUpChanged: (Boolean) -> Unit,
fun UnitSystemSwitchRow(
useUSC: Boolean,
onUseUSCChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.use_usc))
Text(text = "Use USC Units (lbs/inches)")
Switch(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.End),
checked = roundUp,
onCheckedChange = onRoundUpChanged
checked = useUSC,
onCheckedChange = onUseUSCChanged
)
}
}
/**
* Calculates the BMI
*
* Catatan: tambahkan unit test untuk kalkulasi BMI ini
*/
private fun calculateBMI(BmiHeight: Double, BmiWeight: Double = 15.0, roundUp: Boolean): String {
var bmi = BmiWeight / 100 * BmiHeight
if (roundUp) {
bmi = kotlin.math.ceil(bmi)
}
return NumberFormat.getNumberInstance().format(bmi)
}
/**
* Calculates the BMI Category
*
* Catatan: tambahkan unit test untuk kalkulasi BMI ini
* Main BMI Calculator Layout
*/
@Composable
fun BMICalculatorLayout() {
var heightInput by remember { mutableStateOf("") }
var weightInput by remember { mutableStateOf("") }
var useUSC by remember { mutableStateOf(false) }
var bmiResult by remember { mutableStateOf<BMIResult?>(null) }
private fun calculateBMICategory(BmiHeight: Double, BmiWeight: Double = 15.0, roundUp: Boolean): String {
var bmi = BmiWeight / 100 * BmiHeight
if (roundUp) {
bmi = kotlin.math.ceil(bmi)
Column(
modifier = Modifier
.padding(40.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "BMI Calculator",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
// Unit System Switch
UnitSystemSwitchRow(
useUSC = useUSC,
onUseUSCChanged = { useUSC = it },
modifier = Modifier.padding(vertical = 8.dp)
)
// Height Input
OutlinedTextField(
value = heightInput,
onValueChange = { heightInput = it },
label = {
Text(if (useUSC) "Height (inches)" else "Height (meters)")
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
// Weight Input
OutlinedTextField(
value = weightInput,
onValueChange = { weightInput = it },
label = {
Text(if (useUSC) "Weight (lbs)" else "Weight (kg)")
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
// Calculate Button
Button(
onClick = {
val height = heightInput.toDoubleOrNull()
val weight = weightInput.toDoubleOrNull()
if (height != null && weight != null) {
bmiResult = calculateBMI(height, weight, useUSC)
} else {
bmiResult = BMIResult(
bmiValue = 0.0,
category = "",
isValid = false,
errorMessage = "Please enter valid numbers"
)
}
return NumberFormat.getNumberInstance().format(bmi)
},
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
) {
Text("Calculate BMI")
}
// Display Result
bmiResult?.let { result ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
colors = CardDefaults.cardColors(
containerColor = if (result.isValid)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.errorContainer
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
if (result.isValid) {
Text(
text = "Your BMI: ${formatBMIResult(result)}",
style = MaterialTheme.typography.titleLarge
)
Text(
text = "Category: ${result.category}",
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(top = 8.dp)
)
// BMI Info
Spacer(modifier = Modifier.height(8.dp))
Text(
text = getBMICategoryInfo(result.category),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
} else {
Text(
text = "Error: ${result.errorMessage}",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.error
)
}
}
}
}
}
}
/**
* Memvalidasi input tinggi dan berat badan
*/
fun validateBMIInput(height: Double, weight: Double, useUSC: Boolean): Pair<Boolean, String> {
if (!useUSC) {
// SI Units validation
when {
height <= 0 -> return Pair(false, "Tinggi harus lebih dari 0")
height < 0.5 -> return Pair(false, "Tinggi terlalu rendah (minimum 0.5m)")
height > 3.0 -> return Pair(false, "Tinggi tidak wajar (maksimum 3.0m)")
weight <= 0 -> return Pair(false, "Berat harus lebih dari 0")
weight < 20 -> return Pair(false, "Berat terlalu rendah (minimum 20kg)")
weight > 500 -> return Pair(false, "Berat tidak wajar (maksimum 500kg)")
}
} else {
// USC Units validation
when {
height <= 0 -> return Pair(false, "Height must be greater than 0")
height < 20 -> return Pair(false, "Height too low (minimum 20 inches)")
height > 120 -> return Pair(false, "Height unrealistic (maximum 120 inches)")
weight <= 0 -> return Pair(false, "Weight must be greater than 0")
weight < 44 -> return Pair(false, "Weight too low (minimum 44 lbs)")
weight > 1100 -> return Pair(false, "Weight unrealistic (maximum 1100 lbs)")
}
}
return Pair(true, "")
}
/**
* Menghitung BMI berdasarkan sistem unit yang dipilih
*
* Formula USC: BMI = 703 × (weight in lbs / height² in inches)
* Formula SI: BMI = weight in kg / height² in meters
*/
fun calculateBMI(height: Double, weight: Double, useUSC: Boolean): BMIResult {
// Validasi input
val (isValid, errorMessage) = validateBMIInput(height, weight, useUSC)
if (!isValid) {
return BMIResult(
bmiValue = 0.0,
category = "",
isValid = false,
errorMessage = errorMessage
)
}
// Kalkulasi BMI
val bmi = if (useUSC) {
// USC: BMI = 703 × (weight in lbs / height² in inches)
703.0 * (weight / (height * height))
} else {
// SI: BMI = weight in kg / height² in meters
weight / (height * height)
}
// Tentukan kategori BMI
val category = getBMICategory(bmi)
return BMIResult(
bmiValue = bmi,
category = category,
isValid = true,
errorMessage = ""
)
}
/**
* Menentukan kategori BMI berdasarkan standar WHO
*/
fun getBMICategory(bmi: Double): String {
return when {
bmi < 18.5 -> "Underweight"
bmi < 25.0 -> "Normal weight"
bmi < 30.0 -> "Overweight"
else -> "Obese"
}
}
/**
* Memberikan informasi tambahan tentang kategori BMI
*/
fun getBMICategoryInfo(category: String): String {
return when (category) {
"Underweight" -> "BMI < 18.5: Berat badan kurang"
"Normal weight" -> "BMI 18.5-24.9: Berat badan ideal"
"Overweight" -> "BMI 25.0-29.9: Kelebihan berat badan"
"Obese" -> "BMI ≥ 30.0: Obesitas"
else -> ""
}
}
/**
* Format BMI result untuk ditampilkan
*/
fun formatBMIResult(result: BMIResult): String {
return if (result.isValid) {
NumberFormat.getNumberInstance().apply {
minimumFractionDigits = 1
maximumFractionDigits = 1
}.format(result.bmiValue)
} else {
result.errorMessage
}
}
@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
fun BMICalculatorPreview() {
TipTimeTheme {
TipTimeLayout()
BMICalculatorLayout()
}
}