This commit is contained in:
202310715043 MUHAMMAD RAFLY AL FATHIR 2025-11-07 16:52:30 +07:00
parent 099c35f19a
commit 2010a5b54c
7 changed files with 1073 additions and 128 deletions

View File

@ -1,10 +1,47 @@
Kalkulator BMI # BMI Calculator Android App
===============
Silahkan kembangkan aplikasi ini untuk melakukan perhitungan BMI **NPM**: 202310715043
**Nama**: Muhammad Rafly Al Fathir
Petunjuk lebih detil dapat dibaca di ## Deskripsi
https://docs.google.com/document/d/1iGiC0Bg3Bdcd2Maq45TYkCDUkZ5Ql51E/edit?rtpof=true Aplikasi Android untuk menghitung Body Mass Index (BMI) berdasarkan tinggi dan berat badan. Aplikasi ini mendukung dua sistem unit: Metrik (cm, kg) dan US Customary (inches, lbs).
Starter dimodifikasi dan terinspirasi dari: ## Fitur
https://developer.android.com/codelabs/basic-android-compose-calculate-tip#0 - ✅ Perhitungan BMI yang akurat
- ✅ Dukungan untuk sistem Metrik dan USC
- ✅ Validasi input untuk mencegah nilai yang tidak wajar
- ✅ Kategori BMI (Underweight, Normal, Overweight, Obese)
- ✅ UI modern dengan Material Design 3
- ✅ Color coding untuk kategori BMI
- ✅ Informasi lengkap tentang kategori BMI
## Teknologi yang Digunakan
- **Kotlin**: Bahasa pemrograman utama
- **Jetpack Compose**: UI Framework
- **Material Design 3**: Design system
- **JUnit**: Unit testing
## Formula BMI
```
BMI = Berat (kg) / (Tinggi (m))²
```
## Kategori BMI (WHO)
- **Underweight**: BMI < 18.5
- **Normal**: 18.5 ≤ BMI < 25
- **Overweight**: 25 ≤ BMI < 30
- **Obese**: BMI ≥ 30
## Unit Testing
Aplikasi dilengkapi dengan unit test untuk:
- Perhitungan BMI (sistem metrik dan USC)
- Kategori BMI
- Validasi input
- Boundary cases
## Kontribusi & Kredit
Aplikasi ini dikembangkan dengan bantuan Claude ai dalam pembuatan kode, desain antarmuka, dan dokumentasi.
## Lisensi
Apache License 2.0

View File

@ -17,10 +17,10 @@ package com.example.tiptime
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -30,17 +30,28 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState 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.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -48,14 +59,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.graphics.Color
import androidx.compose.ui.res.painterResource 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.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 kotlin.math.pow
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -66,82 +78,294 @@ class MainActivity : ComponentActivity() {
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
TipTimeLayout() // State untuk menentukan halaman mana yang ditampilkan
var showWelcomeScreen by remember { mutableStateOf(true) }
if (showWelcomeScreen) {
// Tampilkan Welcome Screen
WelcomeScreen(
onStartClick = {
showWelcomeScreen = false // Pindah ke halaman BMI Calculator
}
)
} else {
// Tampilkan BMI Calculator dengan tombol back
BMICalculatorLayout(
onBackClick = {
showWelcomeScreen = true // Kembali ke Welcome Screen
}
)
}
} }
} }
} }
} }
} }
/**
* Layout utama untuk aplikasi BMI Calculator
* Menampilkan input field untuk tinggi dan berat badan,
* toggle untuk unit sistem, dan hasil perhitungan BMI
*
* @param onBackClick Callback yang dipanggil ketika tombol back diklik
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TipTimeLayout() { fun BMICalculatorLayout(onBackClick: () -> Unit = {}) {
var amountInput by remember { mutableStateOf("") } var heightInput by remember { mutableStateOf("") }
var tipInput by remember { mutableStateOf("") } var weightInput by remember { mutableStateOf("") }
var roundUp by remember { mutableStateOf(false) } var useUSC by remember { mutableStateOf(false) }
val BmiHeight = amountInput.toDoubleOrNull() ?: 0.0 // Konversi input ke Double atau null jika tidak valid
val BmiWeight = tipInput.toDoubleOrNull() ?: 0.0 val height = heightInput.toDoubleOrNull() ?: 0.0
val bmi = calculateBMI(BmiHeight, BmiWeight, roundUp) val weight = weightInput.toDoubleOrNull() ?: 0.0
val category = calculateBMICategory(BmiHeight, BmiWeight, roundUp)
Column( // Validasi input
modifier = Modifier val inputError = validateInput(height, weight, useUSC)
.statusBarsPadding()
.padding(horizontal = 40.dp) // Hitung BMI dan kategori jika input valid
.verticalScroll(rememberScrollState()) val bmi = if (inputError == null) {
.safeDrawingPadding(), calculateBMI(height, weight, useUSC)
horizontalAlignment = Alignment.CenterHorizontally, } else {
verticalArrangement = Arrangement.Center 0.0
) { }
Text(
text = stringResource(R.string.calculate_tip), val category = if (inputError == null && bmi > 0) {
calculateBMICategory(bmi)
} else {
""
}
val categoryColor = getBMICategoryColor(category)
// Gunakan Scaffold untuk layout dengan TopAppBar
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = "BMI Calculator",
fontWeight = FontWeight.Bold
)
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back to Welcome Screen",
modifier = Modifier.size(24.dp)
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { paddingValues ->
Column(
modifier = Modifier modifier = Modifier
.padding(bottom = 16.dp, top = 40.dp) .fillMaxSize()
.align(alignment = Alignment.Start) .padding(paddingValues)
) .padding(horizontal = 24.dp)
EditNumberField( .verticalScroll(rememberScrollState())
label = R.string.height, .safeDrawingPadding(),
leadingIcon = R.drawable.number, horizontalAlignment = Alignment.CenterHorizontally,
keyboardOptions = KeyboardOptions.Default.copy( verticalArrangement = Arrangement.Top
keyboardType = KeyboardType.Number, ) {
imeAction = ImeAction.Next Spacer(modifier = Modifier.height(16.dp))
),
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)) // Subtitle
Text(
text = "Calculate your Body Mass Index",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(bottom = 24.dp)
.align(alignment = Alignment.Start)
)
// Input Field untuk Tinggi
EditNumberField(
label = if (useUSC) "Height (inches)" else "Height (cm)",
leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Next
),
value = heightInput,
onValueChanged = { heightInput = it },
modifier = Modifier
.padding(bottom = 16.dp)
.fillMaxWidth(),
)
// Input Field untuk Berat
EditNumberField(
label = if (useUSC) "Weight (lbs)" else "Weight (kg)",
leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Done
),
value = weightInput,
onValueChanged = { weightInput = it },
modifier = Modifier
.padding(bottom = 16.dp)
.fillMaxWidth(),
)
// Toggle untuk Unit Sistem
UnitSystemToggleRow(
useUSC = useUSC,
onToggleChanged = { useUSC = it },
modifier = Modifier.padding(bottom = 24.dp)
)
// Tampilkan error jika ada
if (inputError != null) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Text(
text = inputError,
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
// Tampilkan hasil BMI jika valid
if (inputError == null && bmi > 0) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Your BMI",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = String.format("%.1f", bmi),
style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 8.dp)
)
if (category.isNotEmpty()) {
Card(
colors = CardDefaults.cardColors(
containerColor = categoryColor
)
) {
Text(
text = category,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
}
}
// Informasi Kategori BMI
BMICategoryInfo()
}
Spacer(modifier = Modifier.height(40.dp))
}
} }
} }
/**
* Composable untuk menampilkan informasi kategori BMI
*/
@Composable
fun BMICategoryInfo() {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "BMI Categories",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
BMICategoryItem("Underweight", "< 18.5", Color(0xFF2196F3))
BMICategoryItem("Normal", "18.5 - 24.9", Color(0xFF4CAF50))
BMICategoryItem("Overweight", "25.0 - 29.9", Color(0xFFFFA726))
BMICategoryItem("Obese", "≥ 30.0", Color(0xFFF44336))
}
}
}
/**
* Composable untuk menampilkan satu item kategori BMI
*/
@Composable
fun BMICategoryItem(category: String, range: String, color: Color) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Card(
modifier = Modifier.padding(end = 8.dp),
colors = CardDefaults.cardColors(containerColor = color)
) {
Spacer(modifier = Modifier
.height(16.dp)
.padding(horizontal = 8.dp))
}
Text(text = category, style = MaterialTheme.typography.bodyMedium)
}
Text(
text = range,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold
)
}
}
/**
* Composable untuk input field angka dengan label dan icon
*/
@Composable @Composable
fun EditNumberField( fun EditNumberField(
@StringRes label: Int, label: String,
@DrawableRes leadingIcon: Int, @DrawableRes leadingIcon: Int,
keyboardOptions: KeyboardOptions, keyboardOptions: KeyboardOptions,
value: String, value: String,
@ -151,64 +375,163 @@ fun EditNumberField(
TextField( TextField(
value = value, value = value,
singleLine = true, singleLine = true,
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) }, leadingIcon = {
Icon(
painter = painterResource(id = leadingIcon),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
},
modifier = modifier, modifier = modifier,
onValueChange = onValueChanged, onValueChange = onValueChanged,
label = { Text(stringResource(label)) }, label = { Text(label) },
keyboardOptions = keyboardOptions keyboardOptions = keyboardOptions,
colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
)
) )
} }
/**
* Composable untuk row toggle unit sistem (Metrik/USC)
*/
@Composable @Composable
fun RoundTheTipRow( fun UnitSystemToggleRow(
roundUp: Boolean, useUSC: Boolean,
onRoundUpChanged: (Boolean) -> Unit, onToggleChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text(text = stringResource(R.string.use_usc)) Column {
Text(
text = "Unit System",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
text = if (useUSC) "US Customary (in, lbs)" else "Metric (cm, kg)",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch( Switch(
modifier = Modifier checked = useUSC,
.fillMaxWidth() onCheckedChange = onToggleChanged
.wrapContentWidth(Alignment.End),
checked = roundUp,
onCheckedChange = onRoundUpChanged
) )
} }
} }
/** /**
* Calculates the BMI * Validasi input tinggi dan berat badan
* *
* Catatan: tambahkan unit test untuk kalkulasi BMI ini * @param height Tinggi badan dalam cm atau inches
* @param weight Berat badan dalam kg atau lbs
* @param useUSC True jika menggunakan unit USC, false untuk metrik
* @return String error message jika tidak valid, null jika valid
*/ */
private fun calculateBMI(BmiHeight: Double, BmiWeight: Double = 15.0, roundUp: Boolean): String { fun validateInput(height: Double, weight: Double, useUSC: Boolean): String? {
var bmi = BmiWeight / 100 * BmiHeight if (height <= 0 || weight <= 0) {
if (roundUp) { return "Please enter valid height and weight"
bmi = kotlin.math.ceil(bmi)
} }
return NumberFormat.getNumberInstance().format(bmi)
}
/**
* Calculates the BMI Category
*
* Catatan: tambahkan unit test untuk kalkulasi BMI ini
*/
private fun calculateBMICategory(BmiHeight: Double, BmiWeight: Double = 15.0, roundUp: Boolean): String { // Validasi untuk sistem metrik (cm, kg)
var bmi = BmiWeight / 100 * BmiHeight if (!useUSC) {
if (roundUp) { if (height < 50 || height > 300) {
bmi = kotlin.math.ceil(bmi) return "Height must be between 50-300 cm"
}
if (weight < 10 || weight > 500) {
return "Weight must be between 10-500 kg"
}
} }
return NumberFormat.getNumberInstance().format(bmi) // Validasi untuk sistem USC (inches, lbs)
else {
if (height < 20 || height > 120) {
return "Height must be between 20-120 inches"
}
if (weight < 20 || weight > 1100) {
return "Weight must be between 20-1100 lbs"
}
}
return null
} }
/**
* Menghitung BMI (Body Mass Index)
*
* Formula BMI: weight (kg) / (height (m))^2
*
* @param height Tinggi badan dalam cm (metrik) atau inches (USC)
* @param weight Berat badan dalam kg (metrik) atau lbs (USC)
* @param useUSC True jika menggunakan unit USC, false untuk metrik
* @return Nilai BMI
*/
fun calculateBMI(height: Double, weight: Double, useUSC: Boolean): Double {
if (height <= 0 || weight <= 0) return 0.0
val heightInMeters: Double
val weightInKg: Double
if (useUSC) {
// Konversi dari inches ke meter dan lbs ke kg
heightInMeters = height * 0.0254
weightInKg = weight * 0.453592
} else {
// Konversi dari cm ke meter
heightInMeters = height / 100.0
weightInKg = weight
}
// Formula BMI: weight / height^2
return weightInKg / heightInMeters.pow(2)
}
/**
* Menentukan kategori BMI berdasarkan nilai BMI
*
* Kategori WHO:
* - Underweight: BMI < 18.5
* - Normal weight: 18.5 BMI < 25
* - Overweight: 25 BMI < 30
* - Obese: BMI 30
*
* @param bmi Nilai BMI
* @return Kategori BMI dalam bentuk String
*/
fun calculateBMICategory(bmi: Double): String {
return when {
bmi < 18.5 -> "Underweight"
bmi < 25.0 -> "Normal"
bmi < 30.0 -> "Overweight"
else -> "Obese"
}
}
/**
* Mendapatkan warna untuk kategori BMI
*
* @param category Kategori BMI
* @return Color yang sesuai dengan kategori
*/
fun getBMICategoryColor(category: String): Color {
return when (category) {
"Underweight" -> Color(0xFF2196F3) // Blue
"Normal" -> Color(0xFF4CAF50) // Green
"Overweight" -> Color(0xFFFFA726) // Orange
"Obese" -> Color(0xFFF44336) // Red
else -> Color.Gray
}
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun TipTimeLayoutPreview() { fun BMICalculatorLayoutPreview() {
TipTimeTheme { TipTimeTheme {
TipTimeLayout() BMICalculatorLayout()
} }
} }

View File

@ -0,0 +1,236 @@
package com.example.tiptime
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.tiptime.ui.theme.TipTimeTheme
/**
* Composable untuk halaman Welcome Screen
*
* @param onStartClick Callback yang dipanggil ketika tombol "MULAI" diklik
*/
@Composable
fun WelcomeScreen(onStartClick: () -> Unit) {
// Animasi scale untuk efek zoom-in saat halaman muncul
val scale = remember { Animatable(0.8f) }
// Jalankan animasi saat composable pertama kali ditampilkan
LaunchedEffect(Unit) {
scale.animateTo(
targetValue = 1f,
animationSpec = tween(durationMillis = 800)
)
}
// Background dengan warna theme
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp)
.scale(scale.value),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// Icon/Logo Aplikasi
Card(
modifier = Modifier
.size(120.dp)
.padding(bottom = 24.dp),
shape = RoundedCornerShape(60.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = R.drawable.number),
contentDescription = "BMI Calculator Icon",
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary
)
}
}
// Judul Aplikasi
Text(
text = "BMI Calculator",
fontSize = 36.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.Center
)
Text(
text = "Body Mass Index Calculator",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 8.dp, bottom = 32.dp)
)
// Card Biodata Pengembang
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "👨‍💻 Dikembangkan Oleh",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 16.dp)
)
// NPM
InfoRow(
label = "NPM",
value = "202310715043"
)
Spacer(modifier = Modifier.height(12.dp))
// Nama
InfoRow(
label = "Nama",
value = "M Rafly Al Fathir"
)
Spacer(modifier = Modifier.height(12.dp))
// Program Studi (opsional)
InfoRow(
label = "Prodi",
value = "Teknik Informatika"
)
}
}
// Deskripsi Singkat
Text(
text = "Aplikasi untuk menghitung Body Mass Index (BMI) berdasarkan tinggi dan berat badan Anda dengan dukungan sistem Metrik dan US Customary.",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 32.dp)
)
// Tombol MULAI
Button(
onClick = onStartClick,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
),
elevation = ButtonDefaults.buttonElevation(
defaultElevation = 4.dp,
pressedElevation = 8.dp
)
) {
Text(
text = "MULAI",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
letterSpacing = 1.2.sp
)
}
Spacer(modifier = Modifier.height(16.dp))
// Copyright
Text(
text = "© 2024 BMI Calculator App",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
textAlign = TextAlign.Center
)
}
}
}
/**
* Composable untuk menampilkan baris informasi (label: value)
*/
@Composable
fun InfoRow(label: String, value: String) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = label,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Medium
)
Text(
text = value,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 4.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun WelcomeScreenPreview() {
TipTimeTheme {
WelcomeScreen(onStartClick = {})
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M13,6.99h3L12,3L8,6.99h3v10.02H8L12,21l4,-3.99h-3z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,3C10.34,3 9,4.34 9,6c0,1.1 0.6,2.05 1.48,2.58L7.08,18H4v3h16v-3h-3.08l-3.4,-9.42C14.4,8.05 15,7.1 15,6C15,4.34 13.66,3 12,3zM12,5c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1S11.45,5 12,5z"/>
</vector>

View File

@ -1,25 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<resources> <resources>
<string name="app_name">BMI Calculator</string> <string name="app_name">BMI Calculator</string>
<string name="calculate_tip">Calculate BMI</string> <string name="app_title">BMI Calculator</string>
<string name="height">Tinggi Badan</string> <string name="app_subtitle">Calculate your Body Mass Index</string>
<string name="weight">Berat Badan</string>
<string name="use_usc">Gunakan Unit USC (lbs/in)?</string> <!-- Input Labels -->
<string name="bmi_calculation">BMI Anda: %s</string> <string name="height_cm">Height (cm)</string>
<string name="bmi_category">Kategori: %s</string> <string name="height_inches">Height (inches)</string>
</resources> <string name="weight_kg">Weight (kg)</string>
<string name="weight_lbs">Weight (lbs)</string>
<!-- Unit System -->
<string name="unit_system">Unit System</string>
<!-- Results -->
<string name="your_bmi">Your BMI</string>
<string name="bmi_categories_title">BMI Categories</string>
</resources>

View File

@ -0,0 +1,338 @@
/**
* Unit tests untuk BMI Calculator
*
* NPM: [ISI NPM ANDA]
* Nama: [ISI NAMA ANDA]
*
* Deskripsi: File ini berisi unit test untuk menguji fungsi-fungsi
* perhitungan BMI, kategori BMI, dan validasi input.
*
* Cara menjalankan test:
* 1. Di Android Studio, klik kanan pada file ini
* 2. Pilih "Run 'BMICalculatorTest'"
* Atau jalankan via terminal: ./gradlew test
*/
package com.example.tiptime
import org.junit.Test
import org.junit.Assert.*
/**
* Unit Test Class untuk BMI Calculator
* Menguji semua fungsi kalkulasi dan validasi
*/
class BMICalculatorTest {
/**
* Test 1: Menguji perhitungan BMI dengan sistem metrik (cm, kg)
* Input: Tinggi 170 cm, Berat 70 kg
* Expected Output: BMI = 24.22
*/
@Test
fun calculateBMI_metricSystem_returnsCorrectValue() {
// Arrange (Persiapan)
val height = 170.0
val weight = 70.0
val useUSC = false
// Act (Eksekusi)
val result = calculateBMI(height, weight, useUSC)
// Assert (Verifikasi)
// BMI = 70 / (1.7 * 1.7) = 24.22
assertEquals(24.22, result, 0.01)
}
/**
* Test 2: Menguji perhitungan BMI dengan sistem USC (inches, lbs)
* Input: Tinggi 67 inches, Berat 154 lbs
* Expected Output: BMI 24.12
*/
@Test
fun calculateBMI_uscSystem_returnsCorrectValue() {
val height = 67.0
val weight = 154.0
val useUSC = true
val result = calculateBMI(height, weight, useUSC)
// Konversi: 67 in = 1.7018 m, 154 lbs = 69.85 kg
// BMI = 69.85 / (1.7018)^2 = 24.12
assertEquals(24.12, result, 0.1)
}
/**
* Test 3: Menguji edge case - tinggi = 0
* Input: Tinggi 0 cm
* Expected Output: BMI = 0 (untuk menghindari division by zero)
*/
@Test
fun calculateBMI_zeroHeight_returnsZero() {
val result = calculateBMI(0.0, 70.0, false)
assertEquals(0.0, result, 0.01)
}
/**
* Test 4: Menguji edge case - berat = 0
* Input: Berat 0 kg
* Expected Output: BMI = 0
*/
@Test
fun calculateBMI_zeroWeight_returnsZero() {
val result = calculateBMI(170.0, 0.0, false)
assertEquals(0.0, result, 0.01)
}
/**
* Test 5: Menguji kategori BMI - Underweight
* Input: BMI = 17.5
* Expected Output: "Underweight"
*/
@Test
fun calculateBMICategory_underweight_returnsCorrectCategory() {
val bmi = 17.5
val result = calculateBMICategory(bmi)
assertEquals("Underweight", result)
}
/**
* Test 6: Menguji kategori BMI - Normal
* Input: BMI = 22.0
* Expected Output: "Normal"
*/
@Test
fun calculateBMICategory_normal_returnsCorrectCategory() {
val bmi = 22.0
val result = calculateBMICategory(bmi)
assertEquals("Normal", result)
}
/**
* Test 7: Menguji kategori BMI - Overweight
* Input: BMI = 27.5
* Expected Output: "Overweight"
*/
@Test
fun calculateBMICategory_overweight_returnsCorrectCategory() {
val bmi = 27.5
val result = calculateBMICategory(bmi)
assertEquals("Overweight", result)
}
/**
* Test 8: Menguji kategori BMI - Obese
* Input: BMI = 32.0
* Expected Output: "Obese"
*/
@Test
fun calculateBMICategory_obese_returnsCorrectCategory() {
val bmi = 32.0
val result = calculateBMICategory(bmi)
assertEquals("Obese", result)
}
/**
* Test 9: Menguji boundary case - BMI = 18.5 (batas Normal)
* Input: BMI = 18.5
* Expected Output: "Normal"
*/
@Test
fun calculateBMICategory_boundaryCase_18point5_returnsNormal() {
val bmi = 18.5
val result = calculateBMICategory(bmi)
assertEquals("Normal", result)
}
/**
* Test 10: Menguji boundary case - BMI = 25.0 (batas Overweight)
* Input: BMI = 25.0
* Expected Output: "Overweight"
*/
@Test
fun calculateBMICategory_boundaryCase_25_returnsOverweight() {
val bmi = 25.0
val result = calculateBMICategory(bmi)
assertEquals("Overweight", result)
}
/**
* Test 11: Menguji boundary case - BMI = 30.0 (batas Obese)
* Input: BMI = 30.0
* Expected Output: "Obese"
*/
@Test
fun calculateBMICategory_boundaryCase_30_returnsObese() {
val bmi = 30.0
val result = calculateBMICategory(bmi)
assertEquals("Obese", result)
}
/**
* Test 12: Menguji validasi input yang valid (metrik)
* Input: Tinggi 170 cm, Berat 70 kg
* Expected Output: null (tidak ada error)
*/
@Test
fun validateInput_validMetricInput_returnsNull() {
val result = validateInput(170.0, 70.0, false)
assertNull(result)
}
/**
* Test 13: Menguji validasi input yang valid (USC)
* Input: Tinggi 67 inches, Berat 154 lbs
* Expected Output: null (tidak ada error)
*/
@Test
fun validateInput_validUSCInput_returnsNull() {
val result = validateInput(67.0, 154.0, true)
assertNull(result)
}
/**
* Test 14: Menguji validasi input - tinggi = 0
* Input: Tinggi 0 cm
* Expected Output: error message (not null)
*/
@Test
fun validateInput_zeroHeight_returnsError() {
val result = validateInput(0.0, 70.0, false)
assertNotNull(result)
}
/**
* Test 15: Menguji validasi input - berat = 0
* Input: Berat 0 kg
* Expected Output: error message (not null)
*/
@Test
fun validateInput_zeroWeight_returnsError() {
val result = validateInput(170.0, 0.0, false)
assertNotNull(result)
}
/**
* Test 16: Menguji validasi input - tinggi terlalu rendah (metrik)
* Input: Tinggi 40 cm (tidak realistis)
* Expected Output: error message (not null)
*/
@Test
fun validateInput_heightTooLowMetric_returnsError() {
val result = validateInput(40.0, 70.0, false)
assertNotNull(result)
}
/**
* Test 17: Menguji validasi input - tinggi terlalu tinggi (metrik)
* Input: Tinggi 350 cm (tidak realistis)
* Expected Output: error message (not null)
*/
@Test
fun validateInput_heightTooHighMetric_returnsError() {
val result = validateInput(350.0, 70.0, false)
assertNotNull(result)
}
/**
* Test 18: Menguji validasi input - berat terlalu rendah (metrik)
* Input: Berat 5 kg (tidak realistis)
* Expected Output: error message (not null)
*/
@Test
fun validateInput_weightTooLowMetric_returnsError() {
val result = validateInput(170.0, 5.0, false)
assertNotNull(result)
}
/**
* Test 19: Menguji validasi input - berat terlalu tinggi (metrik)
* Input: Berat 600 kg (tidak realistis)
* Expected Output: error message (not null)
*/
@Test
fun validateInput_weightTooHighMetric_returnsError() {
val result = validateInput(170.0, 600.0, false)
assertNotNull(result)
}
/**
* Test 20: Menguji perhitungan BMI dengan nilai real world
* Input: Tinggi 165 cm, Berat 60 kg
* Expected Output: BMI = 22.04
*/
@Test
fun calculateBMI_realWorldExample1_returnsCorrectValue() {
val height = 165.0
val weight = 60.0
val useUSC = false
val result = calculateBMI(height, weight, useUSC)
// BMI = 60 / (1.65 * 1.65) = 22.04
assertEquals(22.04, result, 0.01)
}
/**
* Test 21: Menguji perhitungan BMI dengan nilai real world lainnya
* Input: Tinggi 180 cm, Berat 85 kg
* Expected Output: BMI = 26.23
*/
@Test
fun calculateBMI_realWorldExample2_returnsCorrectValue() {
val height = 180.0
val weight = 85.0
val useUSC = false
val result = calculateBMI(height, weight, useUSC)
// BMI = 85 / (1.8 * 1.8) = 26.23
assertEquals(26.23, result, 0.01)
}
/**
* Test 22: Menguji validasi input dengan nilai boundary - tinggi minimum valid
* Input: Tinggi 50 cm (batas minimum)
* Expected Output: null (valid)
*/
@Test
fun validateInput_minimumHeightMetric_returnsNull() {
val result = validateInput(50.0, 70.0, false)
assertNull(result)
}
/**
* Test 23: Menguji validasi input dengan nilai boundary - tinggi maksimum valid
* Input: Tinggi 300 cm (batas maksimum)
* Expected Output: null (valid)
*/
@Test
fun validateInput_maximumHeightMetric_returnsNull() {
val result = validateInput(300.0, 70.0, false)
assertNull(result)
}
/**
* Test 24: Menguji kategori BMI dengan nilai sangat rendah
* Input: BMI = 10.0
* Expected Output: "Underweight"
*/
@Test
fun calculateBMICategory_veryLowBMI_returnsUnderweight() {
val bmi = 10.0
val result = calculateBMICategory(bmi)
assertEquals("Underweight", result)
}
/**
* Test 25: Menguji kategori BMI dengan nilai sangat tinggi
* Input: BMI = 50.0
* Expected Output: "Obese"
*/
@Test
fun calculateBMICategory_veryHighBMI_returnsObese() {
val bmi = 50.0
val result = calculateBMICategory(bmi)
assertEquals("Obese", result)
}
}