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 package com.example.tiptime
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.*
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.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.material3.*
import androidx.compose.material3.Icon import androidx.compose.runtime.*
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.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.tiptime.ui.theme.TipTimeTheme import com.example.tiptime.ui.theme.TipTimeTheme
import java.text.NumberFormat import java.text.NumberFormat
/**
* BMI Calculator Application
*
* NPM: [202310715051]
* Nama: [Dendi Yogia Pratama]
*/
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent { setContent {
TipTimeTheme { TipTimeTheme {
Surface( Surface(
modifier = Modifier.fillMaxSize(), 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 @Composable
fun TipTimeLayout() { fun UnitSystemSwitchRow(
var amountInput by remember { mutableStateOf("") } useUSC: Boolean,
var tipInput by remember { mutableStateOf("") } onUseUSCChanged: (Boolean) -> Unit,
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,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text(text = stringResource(R.string.use_usc)) Text(text = "Use USC Units (lbs/inches)")
Switch( Switch(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentWidth(Alignment.End), .wrapContentWidth(Alignment.End),
checked = roundUp, checked = useUSC,
onCheckedChange = onRoundUpChanged onCheckedChange = onUseUSCChanged
) )
} }
} }
/** /**
* Calculates the BMI * Main BMI Calculator Layout
*
* 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
*/ */
@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 { Column(
var bmi = BmiWeight / 100 * BmiHeight modifier = Modifier
if (roundUp) { .padding(40.dp)
bmi = kotlin.math.ceil(bmi) .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"
)
}
},
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
)
}
}
}
}
} }
return NumberFormat.getNumberInstance().format(bmi)
} }
/**
* 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) @Preview(showBackground = true)
@Composable @Composable
fun TipTimeLayoutPreview() { fun BMICalculatorPreview() {
TipTimeTheme { TipTimeTheme {
TipTimeLayout() BMICalculatorLayout()
} }
} }