diff --git a/README.md b/README.md index 08d4aa4..3225df6 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f786714..cf6fcac 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -73,7 +73,7 @@ dependencies { implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.core:core-ktx:1.15.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") - + implementation("androidx.appcompat:appcompat:1.7.1") testImplementation("junit:junit:4.13.2") androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01")) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e897dec..08036cb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,15 +26,24 @@ android:supportsRtl="true" android:theme="@style/Theme.TipTime" tools:targetApi="33"> + + + + + + android:theme="@style/Theme.AppCompat.Light.NoActionBar"> + diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..9536c30 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/example/tiptime/MainActivity.kt b/app/src/main/java/com/example/tiptime/MainActivity.kt index d0fdd80..e05f23a 100644 --- a/app/src/main/java/com/example/tiptime/MainActivity.kt +++ b/app/src/main/java/com/example/tiptime/MainActivity.kt @@ -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 import android.os.Bundle @@ -21,31 +6,12 @@ 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 -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.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.unit.dp 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() { override fun onCreate(savedInstanceState: Bundle?) { @@ -63,10 +32,8 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { TipTimeTheme { - Surface( - modifier = Modifier.fillMaxSize(), - ) { - TipTimeLayout() + Surface(modifier = Modifier.fillMaxSize()) { + BmiCalculatorLayout() } } } @@ -74,71 +41,162 @@ class MainActivity : ComponentActivity() { } @Composable -fun TipTimeLayout() { - var amountInput by remember { mutableStateOf("") } - var tipInput by remember { mutableStateOf("") } - var roundUp by remember { mutableStateOf(false) } +fun BmiCalculatorLayout() { + var heightInput by remember { mutableStateOf("") } + var weightInput by remember { mutableStateOf("") } + var errorMessage by remember { mutableStateOf("") } - val BmiHeight = amountInput.toDoubleOrNull() ?: 0.0 - val BmiWeight = tipInput.toDoubleOrNull() ?: 0.0 - val bmi = calculateBMI(BmiHeight, BmiWeight, roundUp) - val category = calculateBMICategory(BmiHeight, BmiWeight, roundUp) + // State untuk hasil BMI + var bmiResult by remember { mutableStateOf(null) } + var bmiCategory by remember { mutableStateOf("") } - Column( + val height = heightInput.toFloatOrNull() ?: 0f + val weight = weightInput.toFloatOrNull() ?: 0f + val isValid = validateInput(height, weight) + + Box( modifier = Modifier - .statusBarsPadding() - .padding(horizontal = 40.dp) + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf( + MaterialTheme.colorScheme.primary.copy(alpha = 0.4f), + MaterialTheme.colorScheme.secondary.copy(alpha = 0.2f) + ) + ) + ) .verticalScroll(rememberScrollState()) - .safeDrawingPadding(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + .padding(horizontal = 24.dp, vertical = 32.dp) ) { - Text( - text = stringResource(R.string.calculate_tip), - 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 - ) + Column(horizontalAlignment = Alignment.CenterHorizontally) { - Spacer(modifier = Modifier.height(150.dp)) + 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( + label = R.string.height, + leadingIcon = R.drawable.number, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Next + ), + value = heightInput, + onValueChanged = { heightInput = it }, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp) + ) + + EditNumberField( + label = R.string.weight, + leadingIcon = R.drawable.number, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + value = weightInput, + onValueChanged = { weightInput = it }, + modifier = Modifier.fillMaxWidth() + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + onClick = { + if (!isValid) { + errorMessage = "Masukkan tinggi (20–250 cm) dan berat (10–250 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 fun EditNumberField( @StringRes label: Int, @@ -150,65 +208,47 @@ fun EditNumberField( ) { TextField( value = value, - singleLine = true, - leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) }, - modifier = modifier, onValueChange = onValueChanged, + singleLine = true, + leadingIcon = { Icon(painter = painterResource(id = leadingIcon), contentDescription = null) }, label = { Text(stringResource(label)) }, - keyboardOptions = keyboardOptions + keyboardOptions = keyboardOptions, + modifier = modifier ) } -@Composable -fun RoundTheTipRow( - roundUp: Boolean, - onRoundUpChanged: (Boolean) -> Unit, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Text(text = stringResource(R.string.use_usc)) - Switch( - modifier = Modifier - .fillMaxWidth() - .wrapContentWidth(Alignment.End), - checked = roundUp, - onCheckedChange = onRoundUpChanged - ) +/** + * Menghitung nilai BMI berdasarkan berat (kg) dan tinggi (cm) + * Rumus: BMI = berat / (tinggi/100)^2 + */ +fun calculateBMI(weight: Float, height: Float): Float { + if (height <= 0) return 0f + return weight / (height / 100).pow(2) +} + +/** + * Menentukan kategori BMI + */ +fun getBMICategory(bmi: Float): String { + return when { + bmi < 18.5 -> "Kurus" + bmi < 25 -> "Normal" + bmi < 30 -> "Kelebihan Berat" + else -> "Obesitas" } } /** - * Calculates the BMI - * - * Catatan: tambahkan unit test untuk kalkulasi BMI ini + * Validasi input agar nilai masuk akal */ -private fun calculateBMI(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) +fun validateInput(height: Float, weight: Float): Boolean { + return height in 20f..250f && weight in 10f..250f } -/** - * 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) @Composable -fun TipTimeLayoutPreview() { +fun BmiCalculatorPreview() { TipTimeTheme { - TipTimeLayout() + BmiCalculatorLayout() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/tiptime/SplashActivity.kt b/app/src/main/java/com/example/tiptime/SplashActivity.kt new file mode 100644 index 0000000..b581b87 --- /dev/null +++ b/app/src/main/java/com/example/tiptime/SplashActivity.kt @@ -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