Penyesuaian Warna ketika Tema Terang dan Penambahan Fitur Validasi Input

This commit is contained in:
202310715297 RAIHAN ARIQ MUZAKKI 2025-11-07 14:12:33 +07:00
parent 010dee7e6b
commit 637c6089b6
2 changed files with 356 additions and 286 deletions

View File

@ -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 50300 cm."
showResult = false
return@Button
}
else if (bmiWeight !in 20.0..500.0) {
errorMessage = "Berat harus berada pada rentang 20500 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 20120 in."
showResult = false
return@Button
}
else if (bmiWeight !in 44.0..1100.0) {
errorMessage = "Berat harus berada pada rentang 441100 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()
}
}
}

View File

@ -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)