This commit is contained in:
RafiFattan23 2025-11-06 17:25:02 +07:00
parent 099c35f19a
commit b186c70643
41 changed files with 523 additions and 374 deletions

View File

@ -1,10 +1,68 @@
Kalkulator BMI # 📱 Aplikasi Kalkulator BMI
===============
Silahkan kembangkan aplikasi ini untuk melakukan perhitungan BMI **Dibuat oleh:**
👨‍💻 **Rafi Fattan Fitriardi**
🆔 **NIM: 202310715002**
🏫 **Pemrograman Perangkat Bergerak - F5A5**
Petunjuk lebih detil dapat dibaca di ---
https://docs.google.com/document/d/1iGiC0Bg3Bdcd2Maq45TYkCDUkZ5Ql51E/edit?rtpof=true
## 📖 Deskripsi Aplikasi
Aplikasi **Kalkulator BMI (Body Mass Index)** ini dibuat sebagai proyek akhir mata kuliah **Pemrograman Perangkat Bergerak**.
Tujuan utama aplikasi ini adalah membantu pengguna menghitung **Indeks Massa Tubuh (BMI)** berdasarkan **berat badan (kg)** dan **tinggi badan (cm)** untuk mengetahui apakah berat badan tergolong **kurang, ideal, berlebih, atau obesitas**.
Aplikasi ini memiliki **dua halaman utama**:
1. **Halaman Biodata Pengembang** menampilkan informasi pembuat aplikasi (nama, NIM, kelas, dan foto), serta tombol **“MULAI”** untuk berpindah ke laman utama.
2. **Halaman Utama (Kalkulator BMI)** tempat pengguna menginput berat dan tinggi badan, menekan tombol **“Hitung BMI”**, lalu melihat hasil nilai BMI beserta kategori dan saran kesehatannya.
---
## ⚙️ Fitur Utama
- Input berat dan tinggi badan secara interaktif.
- Perhitungan otomatis nilai BMI.
- Tampilan kategori hasil (Kurus, Normal, Gemuk, Obesitas).
- Antarmuka sederhana dan responsif.
- Navigasi antarhalaman menggunakan tombol **MULAI** dari halaman biodata.
---
## 🧩 Teknologi yang Digunakan
- **Android Studio (Kotlin)**
- **XML Layouts** untuk desain antarmuka
- **Intent** untuk navigasi antar activity
- **Drawable XML** untuk gradasi dan tema warna aplikasi
---
## 💡 Struktur Proyek
```
app/
├── java/com/example/bmiapp/
│ ├── SplashActivity.kt // Halaman biodata pengembang
│ ├── MainActivity.kt // Halaman utama kalkulator BMI
├── res/
│ ├── layout/
│ │ ├── activity_splash.xml
│ │ ├── activity_main.xml
│ ├── drawable/
│ │ ├── splash_gradient.xml
│ ├── mipmap/
│ │ ├── ic_launcher.png // Ikon aplikasi
│ │ ├── ic_launcher_round.png
│ ├── values/
│ ├── colors.xml
│ ├── strings.xml
│ ├── themes.xml
└── AndroidManifest.xml
```
---
## 🧠 Kontribusi & Kredit
Aplikasi ini dikembangkan dengan bantuan **ChatGPT (OpenAI)** dalam pembuatan kode, desain antarmuka, dan penyusunan dokumentasi.
Semua logika, pengujian, dan penyempurnaan dilakukan secara mandiri oleh pengembang.
---
Starter dimodifikasi dan terinspirasi dari:
https://developer.android.com/codelabs/basic-android-compose-calculate-tip#0

View File

@ -73,7 +73,7 @@ dependencies {
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.core:core-ktx:1.15.0") implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("androidx.appcompat:appcompat:1.7.1")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01")) androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01"))

View File

@ -26,15 +26,24 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.TipTime" android:theme="@style/Theme.TipTime"
tools:targetApi="33"> tools:targetApi="33">
<!-- Laman utama (Kalkulator BMI) -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="false"
android:theme="@style/Theme.TipTime" />
<!-- Laman biodata pengembang (Splash / Intro Screen) -->
<activity
android:name=".SplashActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.TipTime"> android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
</manifest> </manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -1,18 +1,3 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.tiptime package com.example.tiptime
import android.os.Bundle import android.os.Bundle
@ -21,31 +6,12 @@ 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.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.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.material3.Icon import androidx.compose.material3.*
import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.*
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@ -55,7 +21,10 @@ 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
import kotlin.math.roundToInt
import androidx.compose.foundation.background
import androidx.compose.ui.graphics.Brush
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -63,10 +32,8 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
TipTimeTheme { TipTimeTheme {
Surface( Surface(modifier = Modifier.fillMaxSize()) {
modifier = Modifier.fillMaxSize(), BmiCalculatorLayout()
) {
TipTimeLayout()
} }
} }
} }
@ -74,71 +41,162 @@ class MainActivity : ComponentActivity() {
} }
@Composable @Composable
fun TipTimeLayout() { fun BmiCalculatorLayout() {
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 errorMessage by remember { mutableStateOf("") }
val BmiHeight = amountInput.toDoubleOrNull() ?: 0.0 // State untuk hasil BMI
val BmiWeight = tipInput.toDoubleOrNull() ?: 0.0 var bmiResult by remember { mutableStateOf<Float?>(null) }
val bmi = calculateBMI(BmiHeight, BmiWeight, roundUp) var bmiCategory by remember { mutableStateOf("") }
val category = calculateBMICategory(BmiHeight, BmiWeight, roundUp)
Column( val height = heightInput.toFloatOrNull() ?: 0f
val weight = weightInput.toFloatOrNull() ?: 0f
val isValid = validateInput(height, weight)
Box(
modifier = Modifier modifier = Modifier
.statusBarsPadding() .fillMaxSize()
.padding(horizontal = 40.dp) .background(
.verticalScroll(rememberScrollState()) Brush.verticalGradient(
.safeDrawingPadding(), colors = listOf(
horizontalAlignment = Alignment.CenterHorizontally, MaterialTheme.colorScheme.primary.copy(alpha = 0.4f),
verticalArrangement = Arrangement.Center MaterialTheme.colorScheme.secondary.copy(alpha = 0.2f)
) {
Text(
text = stringResource(R.string.calculate_tip),
modifier = Modifier
.padding(bottom = 16.dp, top = 40.dp)
.align(alignment = Alignment.Start)
) )
)
)
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp, vertical = 32.dp)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Kalkulator BMI",
style = MaterialTheme.typography.headlineMedium.copy(
color = MaterialTheme.colorScheme.primary
)
)
}
Spacer(modifier = Modifier.height(24.dp))
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
),
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
EditNumberField( EditNumberField(
label = R.string.height, label = R.string.height,
leadingIcon = R.drawable.number, leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions.Default.copy( keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number, keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next imeAction = ImeAction.Next
), ),
value = amountInput, value = heightInput,
onValueChanged = { amountInput = it }, onValueChanged = { heightInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
) )
EditNumberField( EditNumberField(
label = R.string.weight, label = R.string.weight,
leadingIcon = R.drawable.number, leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions.Default.copy( keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number, keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done imeAction = ImeAction.Done
), ),
value = tipInput, value = weightInput,
onValueChanged = { tipInput = it }, onValueChanged = { weightInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(), modifier = Modifier.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)) Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
if (!isValid) {
errorMessage = "Masukkan tinggi (20250 cm) dan berat (10250 kg) yang valid!"
bmiResult = null
} else {
errorMessage = ""
bmiResult = calculateBMI(weight, height)
bmiCategory = getBMICategory(bmiResult!!)
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
elevation = ButtonDefaults.elevatedButtonElevation(8.dp)
) {
Text(text = "Hitung BMI", style = MaterialTheme.typography.titleMedium)
}
if (errorMessage.isNotEmpty()) {
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 8.dp)
)
}
// Tampilkan hasil hanya jika sudah ditekan
bmiResult?.let { bmi ->
val categoryColor = when (bmiCategory) {
"Kurus" -> MaterialTheme.colorScheme.tertiaryContainer
"Normal" -> MaterialTheme.colorScheme.primaryContainer
"Kelebihan Berat" -> MaterialTheme.colorScheme.secondaryContainer
else -> MaterialTheme.colorScheme.errorContainer
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp),
colors = CardDefaults.cardColors(containerColor = categoryColor),
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(20.dp)
) {
Text(
text = "BMI Anda: %.2f".format(bmi),
style = MaterialTheme.typography.titleLarge
)
Text(
text = "Kategori: $bmiCategory",
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
} }
} }
/**
* Fungsi untuk membuat TextField input angka
*/
@Composable @Composable
fun EditNumberField( fun EditNumberField(
@StringRes label: Int, @StringRes label: Int,
@ -150,65 +208,47 @@ fun EditNumberField(
) { ) {
TextField( TextField(
value = value, value = value,
singleLine = true,
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
modifier = modifier,
onValueChange = onValueChanged, onValueChange = onValueChanged,
singleLine = true,
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), contentDescription = null) },
label = { Text(stringResource(label)) }, label = { Text(stringResource(label)) },
keyboardOptions = keyboardOptions keyboardOptions = keyboardOptions,
modifier = modifier
) )
} }
@Composable /**
fun RoundTheTipRow( * Menghitung nilai BMI berdasarkan berat (kg) dan tinggi (cm)
roundUp: Boolean, * Rumus: BMI = berat / (tinggi/100)^2
onRoundUpChanged: (Boolean) -> Unit, */
modifier: Modifier = Modifier fun calculateBMI(weight: Float, height: Float): Float {
) { if (height <= 0) return 0f
Row( return weight / (height / 100).pow(2)
modifier = modifier.fillMaxWidth(), }
verticalAlignment = Alignment.CenterVertically
) { /**
Text(text = stringResource(R.string.use_usc)) * Menentukan kategori BMI
Switch( */
modifier = Modifier fun getBMICategory(bmi: Float): String {
.fillMaxWidth() return when {
.wrapContentWidth(Alignment.End), bmi < 18.5 -> "Kurus"
checked = roundUp, bmi < 25 -> "Normal"
onCheckedChange = onRoundUpChanged bmi < 30 -> "Kelebihan Berat"
) else -> "Obesitas"
} }
} }
/** /**
* Calculates the BMI * Validasi input agar nilai masuk akal
*
* Catatan: tambahkan unit test untuk kalkulasi BMI ini
*/ */
private fun calculateBMI(BmiHeight: Double, BmiWeight: Double = 15.0, roundUp: Boolean): String { fun validateInput(height: Float, weight: Float): Boolean {
var bmi = BmiWeight / 100 * BmiHeight return height in 20f..250f && weight in 10f..250f
if (roundUp) {
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 {
var bmi = BmiWeight / 100 * BmiHeight
if (roundUp) {
bmi = kotlin.math.ceil(bmi)
}
return NumberFormat.getNumberInstance().format(bmi)
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun TipTimeLayoutPreview() { fun BmiCalculatorPreview() {
TipTimeTheme { TipTimeTheme {
TipTimeLayout() BmiCalculatorLayout()
} }
} }

View File

@ -0,0 +1,21 @@
package com.example.tiptime
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
val btnMulai = findViewById<Button>(R.id.btnMulai)
btnMulai.setOnClickListener {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
//finish() // supaya tidak bisa kembali ke splash dengan tombol back
}
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Lapisan gradasi -->
<item>
<shape android:shape="rectangle">
<gradient
android:startColor="#6A11CB"
android:endColor="#2575FC"
android:angle="45" />
</shape>
</item>
<!-- Lapisan overlay transparan -->
<item>
<shape android:shape="rectangle">
<solid android:color="#40FFFFFF" />
</shape>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -1,185 +1,74 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <vector
~ 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 xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp" android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> xmlns:android="http://schemas.android.com/apk/res/android">
<path <path android:fillColor="#3DDC84"
android:fillColor="#3DDC84" android:pathData="M0,0h108v108h-108z"/>
android:pathData="M0,0h108v108h-108z" /> <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:pathData="M9,0L9,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M19,0L19,108" <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:pathData="M29,0L29,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M39,0L39,108" <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:pathData="M49,0L49,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M59,0L59,108" <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:pathData="M69,0L69,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M79,0L79,108" <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:pathData="M89,0L89,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M99,0L99,108" <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:pathData="M0,9L108,9" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,19L108,19" <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:pathData="M0,29L108,29" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector> </vector>

View File

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/transparent" />
<stroke android:width="2dp" android:color="@android:color/white" />
</shape>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:type="linear"
android:angle="270"
android:startColor="#4A90E2"
android:centerColor="#6A5ACD"
android:endColor="#8A2BE2"
android:useLevel="false" />
</shape>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="24dp"
android:background="@drawable/splash_gradient">
<ImageView
android:layout_width="150dp"
android:layout_height="150dp"
android:src="@mipmap/ic_launcher_round"
android:contentDescription="Foto Profil"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rafi Fattan Fitriardi"
android:textStyle="bold"
android:textSize="20sp"
android:textColor="#FFFFFF"
android:shadowColor="#80000000"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="2"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvNim"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="202310715002"
android:textSize="16sp"
android:textColor="#E0E0E0"
android:shadowColor="#80000000"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="2"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/tvKampus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pemrograman Perangkat Bergerak - F5A5"
android:textSize="16sp"
android:textColor="#E0E0E0"
android:shadowColor="#80000000"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="2"
android:layout_marginBottom="24dp" />
<Button
android:id="@+id/btnMulai"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MULAI"
android:backgroundTint="#4CAF50"
android:textColor="@android:color/white"
android:paddingHorizontal="32dp"
android:paddingVertical="12dp" />
</LinearLayout>

View File

@ -1,22 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

View File

@ -1,22 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,63 @@
package com.example.tiptime
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Unit test untuk memastikan fungsi perhitungan dan kategori BMI
* berjalan dengan benar.
*
* Lokasi file: app/src/test/java/com/example/tiptime/BmiUnitTest.kt
*/
class BmiUnitTest {
@Test
fun calculateBMI_isAccurate() {
// Data: berat 70 kg, tinggi 170 cm
val result = calculateBMI(70f, 170f)
// Hasil ekspektasi ≈ 24.22
assertEquals(24.22f, result, 0.01f)
}
@Test
fun calculateBMI_zeroHeight_returnsZero() {
val result = calculateBMI(70f, 0f)
assertEquals(0f, result, 0.0f)
}
@Test
fun getBMICategory_underweight_returnsKurus() {
val category = getBMICategory(17.0f)
assertEquals("Kurus", category)
}
@Test
fun getBMICategory_normal_returnsNormal() {
val category = getBMICategory(22.5f)
assertEquals("Normal", category)
}
@Test
fun getBMICategory_overweight_returnsKelebihanBerat() {
val category = getBMICategory(27.5f)
assertEquals("Kelebihan Berat", category)
}
@Test
fun getBMICategory_obese_returnsObesitas() {
val category = getBMICategory(31.0f)
assertEquals("Obesitas", category)
}
@Test
fun validateInput_validValues_returnsTrue() {
val isValid = validateInput(170f, 65f)
assertEquals(true, isValid)
}
@Test
fun validateInput_invalidValues_returnsFalse() {
val isValid = validateInput(5f, 500f)
assertEquals(false, isValid)
}
}