From 2010a5b54ca6e89c90eca2947a8711d688b387e5 Mon Sep 17 00:00:00 2001
From: 202310715043 MUHAMMAD RAFLY AL FATHIR
<202310715043@mhs.ubharajaya.ac.id>
Date: Fri, 7 Nov 2025 16:52:30 +0700
Subject: [PATCH] UTS
---
README.md | 51 +-
.../java/com/example/tiptime/MainActivity.kt | 519 ++++++++++++++----
.../java/com/example/tiptime/WelcomeScreen.kt | 236 ++++++++
app/src/main/res/drawable/ic_height.xml | 9 +
app/src/main/res/drawable/ic_weight.xml | 9 +
app/src/main/res/values/strings.xml | 39 +-
.../com/example/tiptime/BMICalculatorTest.kt | 338 ++++++++++++
7 files changed, 1073 insertions(+), 128 deletions(-)
create mode 100644 app/src/main/java/com/example/tiptime/WelcomeScreen.kt
create mode 100644 app/src/main/res/drawable/ic_height.xml
create mode 100644 app/src/main/res/drawable/ic_weight.xml
create mode 100644 app/src/test/java/com/example/tiptime/BMICalculatorTest.kt
diff --git a/README.md b/README.md
index 08d4aa4..e9214d0 100644
--- a/README.md
+++ b/README.md
@@ -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
-https://docs.google.com/document/d/1iGiC0Bg3Bdcd2Maq45TYkCDUkZ5Ql51E/edit?rtpof=true
+## Deskripsi
+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:
-https://developer.android.com/codelabs/basic-android-compose-calculate-tip#0
\ No newline at end of file
+## Fitur
+- ✅ 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
\ No newline at end of file
diff --git a/app/src/main/java/com/example/tiptime/MainActivity.kt b/app/src/main/java/com/example/tiptime/MainActivity.kt
index d0fdd80..64d1c12 100644
--- a/app/src/main/java/com/example/tiptime/MainActivity.kt
+++ b/app/src/main/java/com/example/tiptime/MainActivity.kt
@@ -17,10 +17,10 @@ package com.example.tiptime
import android.os.Bundle
import androidx.activity.ComponentActivity
+import androidx.activity.compose.BackHandler
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
@@ -30,17 +30,28 @@ 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.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
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.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
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.getValue
import androidx.compose.runtime.mutableStateOf
@@ -48,14 +59,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.tiptime.ui.theme.TipTimeTheme
-import java.text.NumberFormat
+import kotlin.math.pow
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -66,82 +78,294 @@ class MainActivity : ComponentActivity() {
Surface(
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
-fun TipTimeLayout() {
- var amountInput by remember { mutableStateOf("") }
- var tipInput by remember { mutableStateOf("") }
- var roundUp by remember { mutableStateOf(false) }
+fun BMICalculatorLayout(onBackClick: () -> Unit = {}) {
+ var heightInput by remember { mutableStateOf("") }
+ var weightInput by remember { mutableStateOf("") }
+ var useUSC 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)
+ // Konversi input ke Double atau null jika tidak valid
+ val height = heightInput.toDoubleOrNull() ?: 0.0
+ val weight = weightInput.toDoubleOrNull() ?: 0.0
- Column(
- modifier = Modifier
- .statusBarsPadding()
- .padding(horizontal = 40.dp)
- .verticalScroll(rememberScrollState())
- .safeDrawingPadding(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- Text(
- text = stringResource(R.string.calculate_tip),
+ // Validasi input
+ val inputError = validateInput(height, weight, useUSC)
+
+ // Hitung BMI dan kategori jika input valid
+ val bmi = if (inputError == null) {
+ calculateBMI(height, weight, useUSC)
+ } else {
+ 0.0
+ }
+
+ 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
- .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
- )
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(horizontal = 24.dp)
+ .verticalScroll(rememberScrollState())
+ .safeDrawingPadding(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top
+ ) {
+ Spacer(modifier = Modifier.height(16.dp))
- 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
fun EditNumberField(
- @StringRes label: Int,
+ label: String,
@DrawableRes leadingIcon: Int,
keyboardOptions: KeyboardOptions,
value: String,
@@ -151,64 +375,163 @@ fun EditNumberField(
TextField(
value = value,
singleLine = true,
- leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(id = leadingIcon),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ },
modifier = modifier,
onValueChange = onValueChanged,
- label = { Text(stringResource(label)) },
- keyboardOptions = keyboardOptions
+ label = { Text(label) },
+ keyboardOptions = keyboardOptions,
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = MaterialTheme.colorScheme.surface,
+ unfocusedContainerColor = MaterialTheme.colorScheme.surface,
+ )
)
}
+/**
+ * Composable untuk row toggle unit sistem (Metrik/USC)
+ */
@Composable
-fun RoundTheTipRow(
- roundUp: Boolean,
- onRoundUpChanged: (Boolean) -> Unit,
+fun UnitSystemToggleRow(
+ useUSC: Boolean,
+ onToggleChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(
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(
- modifier = Modifier
- .fillMaxWidth()
- .wrapContentWidth(Alignment.End),
- checked = roundUp,
- onCheckedChange = onRoundUpChanged
+ checked = useUSC,
+ onCheckedChange = onToggleChanged
)
}
}
/**
- * 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 {
- var bmi = BmiWeight / 100 * BmiHeight
- if (roundUp) {
- bmi = kotlin.math.ceil(bmi)
+fun validateInput(height: Double, weight: Double, useUSC: Boolean): String? {
+ if (height <= 0 || weight <= 0) {
+ return "Please enter valid height and weight"
}
- 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 {
- var bmi = BmiWeight / 100 * BmiHeight
- if (roundUp) {
- bmi = kotlin.math.ceil(bmi)
+ // Validasi untuk sistem metrik (cm, kg)
+ if (!useUSC) {
+ if (height < 50 || height > 300) {
+ 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)
@Composable
-fun TipTimeLayoutPreview() {
+fun BMICalculatorLayoutPreview() {
TipTimeTheme {
- TipTimeLayout()
+ BMICalculatorLayout()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/tiptime/WelcomeScreen.kt b/app/src/main/java/com/example/tiptime/WelcomeScreen.kt
new file mode 100644
index 0000000..e431919
--- /dev/null
+++ b/app/src/main/java/com/example/tiptime/WelcomeScreen.kt
@@ -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 = {})
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_height.xml b/app/src/main/res/drawable/ic_height.xml
new file mode 100644
index 0000000..a7bd381
--- /dev/null
+++ b/app/src/main/res/drawable/ic_height.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_weight.xml b/app/src/main/res/drawable/ic_weight.xml
new file mode 100644
index 0000000..5920585
--- /dev/null
+++ b/app/src/main/res/drawable/ic_weight.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 04e6db2..05341a5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,25 +1,18 @@
-
-
BMI Calculator
- Calculate BMI
- Tinggi Badan
- Berat Badan
- Gunakan Unit USC (lbs/in)?
- BMI Anda: %s
- Kategori: %s
-
+ BMI Calculator
+ Calculate your Body Mass Index
+
+
+ Height (cm)
+ Height (inches)
+ Weight (kg)
+ Weight (lbs)
+
+
+ Unit System
+
+
+ Your BMI
+ BMI Categories
+
\ No newline at end of file
diff --git a/app/src/test/java/com/example/tiptime/BMICalculatorTest.kt b/app/src/test/java/com/example/tiptime/BMICalculatorTest.kt
new file mode 100644
index 0000000..92abe46
--- /dev/null
+++ b/app/src/test/java/com/example/tiptime/BMICalculatorTest.kt
@@ -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)
+ }
+}
\ No newline at end of file