Compare commits

...

11 Commits

Author SHA1 Message Date
2010a5b54c UTS 2025-11-07 16:52:30 +07:00
099c35f19a Update README.md 2025-11-06 12:04:44 +07:00
0ee43c2e9a First update calculation 2025-11-06 11:34:31 +07:00
7053fa6573 First update labels 2025-11-06 11:10:35 +07:00
51c9a8e5ff First commit 2025-11-06 09:58:07 +07:00
renovate[bot]
b029b3dd10
Update all dependencies 8.7.3 to v8.8.0 (#271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-10 05:25:39 +00:00
renovate[bot]
90545a4a4d
Update dependency gradle 8.11.1 to v8.12 (#263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-21 03:18:17 +00:00
renovate[bot]
e075e6ded8
Update dependency androidx.compose:compose-bom 2024.11.00 to v2024.12.01 (#260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-12 03:31:35 +00:00
Tomáš Mlynarič
7db6c366a3 Update to kotlin 2.1.0 2024-12-09 17:16:15 +01:00
Jose Alcérreca
076428c67e
Merge pull request #257 from google-developer-training/mlykotom-renovate-fix
Add branches to renovate
2024-12-09 13:44:24 +00:00
Tomáš Mlynarič
59c134aec9
Add branches to renovate 2024-12-09 14:35:04 +01:00
14 changed files with 1095 additions and 170 deletions

View File

@ -2,5 +2,10 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>android/.github:renovate-config"
],
"baseBranches": [
"main",
"starter",
"state"
]
}

View File

@ -1,24 +1,47 @@
Tip Time - Solution Code
=================================
# BMI Calculator Android App
Solution code for the [Android Basics with Compose](https://developer.android.com/courses/android-basics-compose/course): Tip Time app.
**NPM**: 202310715043
**Nama**: Muhammad Rafly Al Fathir
## 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).
## 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.
Introduction
------------
The Tip Time app contains various UI elements for calculating a tip,
teaching about user input, and State in Compose.
Pre-requisites
--------------
* Experience with Kotlin syntax.
* How to create and run a project in Android Studio.
Getting Started
---------------
1. Install Android Studio, if you don't already have it.
2. Download the sample.
3. Import the sample into Android Studio.
4. Build and run the sample.
## Lisensi
Apache License 2.0

View File

@ -17,6 +17,7 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
@ -45,18 +46,15 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = JavaVersion.VERSION_17.toString()
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = rootProject.extra["compose_compiler_version"].toString()
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
@ -67,7 +65,7 @@ android {
dependencies {
implementation(platform("androidx.compose:compose-bom:2024.11.00"))
implementation(platform("androidx.compose:compose-bom:2024.12.01"))
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui")
@ -78,7 +76,7 @@ dependencies {
testImplementation("junit:junit:4.13.2")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.11.00"))
androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1")

View File

@ -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,76 +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 amount = amountInput.toDoubleOrNull() ?: 0.0
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount, tipPercent, roundUp)
// Konversi input ke Double atau null jika tidak valid
val height = heightInput.toDoubleOrNull() ?: 0.0
val weight = weightInput.toDoubleOrNull() ?: 0.0
// 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
.statusBarsPadding()
.padding(horizontal = 40.dp)
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
verticalArrangement = Arrangement.Top
) {
Spacer(modifier = Modifier.height(16.dp))
// Subtitle
Text(
text = stringResource(R.string.calculate_tip),
text = "Calculate your Body Mass Index",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(bottom = 16.dp, top = 40.dp)
.padding(bottom = 24.dp)
.align(alignment = Alignment.Start)
)
// Input Field untuk Tinggi
EditNumberField(
label = R.string.bill_amount,
leadingIcon = R.drawable.money,
label = if (useUSC) "Height (inches)" else "Height (cm)",
leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Next
),
value = amountInput,
onValueChanged = { amountInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(),
value = heightInput,
onValueChanged = { heightInput = it },
modifier = Modifier
.padding(bottom = 16.dp)
.fillMaxWidth(),
)
// Input Field untuk Berat
EditNumberField(
label = R.string.how_was_the_service,
leadingIcon = R.drawable.percent,
label = if (useUSC) "Weight (lbs)" else "Weight (kg)",
leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Done
),
value = tipInput,
onValueChanged = { tipInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(),
value = weightInput,
onValueChanged = { weightInput = it },
modifier = Modifier
.padding(bottom = 16.dp)
.fillMaxWidth(),
)
RoundTheTipRow(
roundUp = roundUp,
onRoundUpChanged = { roundUp = it },
modifier = Modifier.padding(bottom = 32.dp)
// 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 = stringResource(R.string.tip_amount, tip),
style = MaterialTheme.typography.displaySmall
text = inputError,
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(150.dp))
}
}
// 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,
@ -145,52 +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.round_up_tip))
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 tip based on the user input and format the tip amount
* according to the local currency.
* Example would be "$10.00".
* Validasi input tinggi dan berat badan
*
* @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 calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
var tip = tipPercent / 100 * amount
if (roundUp) {
tip = kotlin.math.ceil(tip)
fun validateInput(height: Double, weight: Double, useUSC: Boolean): String? {
if (height <= 0 || weight <= 0) {
return "Please enter valid height and weight"
}
// 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"
}
}
// 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
}
return NumberFormat.getCurrencyInstance().format(tip)
}
@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
fun BMICalculatorLayoutPreview() {
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,23 +0,0 @@
<!--
~ 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.
-->
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7.5,11C9.43,11 11,9.43 11,7.5S9.43,4 7.5,4S4,5.57 4,7.5S5.57,11 7.5,11zM7.5,6C8.33,6 9,6.67 9,7.5S8.33,9 7.5,9S6,8.33 6,7.5S6.67,6 7.5,6z"/>
<path android:fillColor="@android:color/white" android:pathData="M4.0025,18.5831l14.5875,-14.5875l1.4142,1.4142l-14.5875,14.5875z"/>
<path android:fillColor="@android:color/white" android:pathData="M16.5,13c-1.93,0 -3.5,1.57 -3.5,3.5s1.57,3.5 3.5,3.5s3.5,-1.57 3.5,-3.5S18.43,13 16.5,13zM16.5,18c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5S17.33,18 16.5,18z"/>
</vector>

View File

@ -1,24 +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>
<string name="app_name">Tip Time</string>
<string name="calculate_tip">Calculate Tip</string>
<string name="bill_amount">Bill Amount</string>
<string name="how_was_the_service">Tip Percentage</string>
<string name="round_up_tip">Round up tip?</string>
<string name="tip_amount">Tip Amount: %s</string>
<string name="app_name">BMI Calculator</string>
<string name="app_title">BMI Calculator</string>
<string name="app_subtitle">Calculate your Body Mass Index</string>
<!-- Input Labels -->
<string name="height_cm">Height (cm)</string>
<string name="height_inches">Height (inches)</string>
<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)
}
}

View File

@ -14,14 +14,10 @@
* limitations under the License.
*/
buildscript {
extra.apply {
set("compose_compiler_version", "1.5.3")
}
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.7.3" apply false
id("com.android.library") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "1.9.10" apply false
id("com.android.application") version "8.13.0" apply false
id("com.android.library") version "8.13.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false
}

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

3
gradlew vendored
View File

@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum