From f17e5803978bdd3c578de2af6fc89882db35883c Mon Sep 17 00:00:00 2001
From: 202310715096-JEREMIA-SEBASTIAN-MARPAUNG
<202310715096@mhs.ubharajaya.ac.id>
Date: Fri, 7 Nov 2025 13:24:45 +0700
Subject: [PATCH] push
---
README.md | 73 ++-
app/build.gradle.kts | 54 +--
app/src/main/AndroidManifest.xml | 24 +-
.../java/com/example/BMIcalculatorScreen.kt | 3 +
.../MainActivity.kt | 422 ++++++++++++++++++
.../java/com/example/tiptime/MainActivity.kt | 196 --------
.../com/example/tiptime/ui/theme/Theme.kt | 115 -----
app/src/main/res/values/strings.xml | 4 +-
8 files changed, 508 insertions(+), 383 deletions(-)
create mode 100644 app/src/main/java/com/example/BMIcalculatorScreen.kt
create mode 100644 app/src/main/java/com/example/basic_android_kotlin_compose_training_tip_calculator/MainActivity.kt
delete mode 100644 app/src/main/java/com/example/tiptime/MainActivity.kt
delete mode 100644 app/src/main/java/com/example/tiptime/ui/theme/Theme.kt
diff --git a/README.md b/README.md
index 4ab87ef..2298bd6 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,59 @@
-Tip Time - Solution Code
-=================================
+# Aplikasi Kalkulator BMI
-Solution code for the [Android Basics with Compose](https://developer.android.com/courses/android-basics-compose/course): Tip Time app.
+**Dibuat oleh:**
+**Nama:** Jeremia Sebastian Marpaung
+**NPM:** 202310715096
+**Kelas:** Pemrograman Perangkat Bergerak - F5A5
+
+---
+
+## Deskripsi Aplikasi
+Aplikasi **Kalkulator BMI (Body Mass Index)** ini dibuat sebagai proyek akhir mata kuliah **Pemrograman Perangkat Bergerak**.
+Tujuannya adalah membantu pengguna menghitung **Indeks Massa Tubuh (BMI)** berdasarkan **berat badan (kg/lbs)** dan **tinggi badan (cm/inci)** agar dapat mengetahui apakah berat badan tergolong **Kurus(Underweight),Normal,Overweight,Obesitas**.
+
+Aplikasi memiliki **satu halaman utama**:
+
+1.**Halaman Utama (Kalkulator BMI)**
+ Pengguna dapat menginput berat dan tinggi badan, menekan tombol **“Hitung BMI”**, dan melihat hasil nilai BMI beserta **kategori serta saran kesehatannya**.
+
+---
+
+## Fitur Utama
+- Input berat dan tinggi badan secara interaktif (bisa satuan **SI** atau **USC**).
+- Perhitungan otomatis nilai BMI dengan opsi pembulatan hasil.
+- Tampilan kategori hasil (Kurus(Underweight), Normal, Overweight, Obesitas).
+- Antarmuka sederhana, bersih, dan responsif menggunakan **Jetpack Compose**.
+- Navigasi antarhalaman dengan tombol **MULAI** dari halaman biodata.
+
+---
+
+## Teknologi yang Digunakan
+- **Android Studio (Kotlin)**
+- **Jetpack Compose** & **XML Layouts** untuk desain antarmuka
+- **Intent** untuk navigasi antar activity
+- **Drawable XML & colors.xml** untuk tema warna dan efek gradasi
+- **Unit Test (disarankan)** untuk menguji akurasi perhitungan BMI
+
+---
+
+## Kontribusi & Kredit
+Aplikasi ini dikembangkan dengan bantuan **Claude.ai** dalam pembuatan kode, desain antarmuka, dan dokumentasi.
+Semua logika perhitungan, pengujian, dan penyempurnaan dilakukan mandiri oleh pengembang.
+
+---
+
+## Change Log (Ringkas)
+- **Migrasi kode dasar** dari kalkulator tip ke kalkulator BMI berbasis Kotlin Compose.
+- **Penambahan mode satuan USC (Inci & Lbs)** dengan validasi tinggi minimal 4 inci.
+- **Perbaikan rumus perhitungan BMI** agar sesuai standar WHO.
+- **Desain ulang Splash Screen** dengan tombol “MULAI” berwarna hijau dan latar gradasi biru-hijau.
+- **Optimalisasi UX** — hasil BMI hanya muncul setelah tombol **“Hitung BMI”** ditekan.
-Introduction
-------------
-The Tip Time app contains various UI elements for calculating a tip,
-teaching about user input, and State in Compose.
+---
+## Lisensi
+Proyek ini dibuat untuk tujuan pembelajaran dalam mata kuliah **Pemrograman Perangkat Bergerak** dan tidak untuk tujuan komersial.
+Lisensi mengikuti [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
-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.
+---
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f786714..6ee5e03 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,19 +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.
- */
-
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
@@ -21,6 +5,7 @@ plugins {
}
android {
+ namespace = "com.example.tiptime"
compileSdk = 35
defaultConfig {
@@ -45,41 +30,46 @@ android {
)
}
}
+
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
+
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_17.toString()
+ jvmTarget = "17"
}
+
buildFeatures {
compose = true
}
+
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
- namespace = "com.example.tiptime"
}
dependencies {
+ // Compose BOM
+ implementation(platform("androidx.compose:compose-bom:2024.02.00"))
- implementation(platform("androidx.compose:compose-bom:2024.12.01"))
- implementation("androidx.activity:activity-compose:1.9.3")
- implementation("androidx.compose.material3:material3")
+ // Jetpack Compose
implementation("androidx.compose.ui:ui")
- implementation("androidx.compose.ui:ui-tooling")
+ implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
- implementation("androidx.core:core-ktx:1.15.0")
- implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ implementation("androidx.compose.material:material-icons-extended")
+ // AndroidX
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+ implementation("androidx.activity:activity-compose:1.8.2")
+ implementation("androidx.navigation:navigation-compose:2.7.7")
+
+ // Testing
testImplementation("junit:junit:4.13.2")
-
- 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")
-
- debugImplementation("androidx.compose.ui:ui-test-manifest")
-}
+ androidTestImplementation("androidx.test.ext:junit:1.1.5")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e897dec..0a60381 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,20 +1,4 @@
-
-
@@ -24,12 +8,12 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
- android:theme="@style/Theme.TipTime"
+ android:theme="@android:style/Theme.Material.Light.NoActionBar"
tools:targetApi="33">
+ android:theme="@android:style/Theme.Material.Light.NoActionBar">
@@ -37,4 +21,4 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/BMIcalculatorScreen.kt b/app/src/main/java/com/example/BMIcalculatorScreen.kt
new file mode 100644
index 0000000..076b6a9
--- /dev/null
+++ b/app/src/main/java/com/example/BMIcalculatorScreen.kt
@@ -0,0 +1,3 @@
+package com.example
+
+annotation class BMIcalculatorScreen
diff --git a/app/src/main/java/com/example/basic_android_kotlin_compose_training_tip_calculator/MainActivity.kt b/app/src/main/java/com/example/basic_android_kotlin_compose_training_tip_calculator/MainActivity.kt
new file mode 100644
index 0000000..5c6482c
--- /dev/null
+++ b/app/src/main/java/com/example/basic_android_kotlin_compose_training_tip_calculator/MainActivity.kt
@@ -0,0 +1,422 @@
+package com.example.basic_android_kotlin_compose_training_tip_calculator
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import kotlin.math.round
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ BMICalculatorApp()
+ }
+ }
+}
+
+@Composable
+fun BMICalculatorApp() {
+ val navController = rememberNavController()
+
+ NavHost(
+ navController = navController,
+ startDestination = "home"
+ ) {
+ composable("home") { HomePage(navController) }
+ composable("calculator") { CalculatorPage(navController) }
+ composable("info") { InfoPage(navController) }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun HomePage(navController: NavHostController) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("BMI Calculator") },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = Color(0xFF00BCD4),
+ titleContentColor = Color.White
+ )
+ )
+ }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ "Selamat Datang!",
+ fontSize = 32.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color(0xFF6200EE)
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ "Hitung BMI Anda dengan mudah",
+ fontSize = 18.sp,
+ color = Color.Gray
+ )
+
+ Spacer(modifier = Modifier.height(48.dp))
+
+ Button(
+ onClick = { navController.navigate("calculator") },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color(0xFF6200EE)
+ )
+ ) {
+ Text("Mulai Hitung BMI", fontSize = 18.sp)
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ OutlinedButton(
+ onClick = { navController.navigate("info") },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Icon(Icons.Default.Info, contentDescription = null)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text("Informasi BMI", fontSize = 18.sp)
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CalculatorPage(navController: NavHostController) {
+ var unitSystem by remember { mutableStateOf("Metric") }
+ var weight by remember { mutableStateOf("") }
+ var height by remember { mutableStateOf("") }
+ var feet by remember { mutableStateOf("") }
+ var inches by remember { mutableStateOf("") }
+ var bmi by remember { mutableDoubleStateOf(0.0) }
+ var category by remember { mutableStateOf("") }
+ var color by remember { mutableStateOf(Color.Gray) }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Kalkulator BMI") },
+ navigationIcon = {
+ IconButton(onClick = { navController.popBackStack() }) {
+ Icon(Icons.Default.ArrowBack, contentDescription = "Back", tint = Color.White)
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = Color(0xFF6200EE),
+ titleContentColor = Color.White
+ )
+ )
+ }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // Unit System Selector
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ FilterChip(
+ selected = unitSystem == "Metric",
+ onClick = {
+ unitSystem = "Metric"
+ weight = ""
+ height = ""
+ bmi = 0.0
+ },
+ label = { Text("Metric (kg/cm)") },
+ modifier = Modifier.weight(1f).padding(end = 8.dp)
+ )
+
+ FilterChip(
+ selected = unitSystem == "USC",
+ onClick = {
+ unitSystem = "USC"
+ weight = ""
+ feet = ""
+ inches = ""
+ bmi = 0.0
+ },
+ label = { Text("USC (lbs/ft/in)") },
+ modifier = Modifier.weight(1f).padding(start = 8.dp)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ if (unitSystem == "Metric") {
+ // Metric System
+ OutlinedTextField(
+ value = weight,
+ onValueChange = { weight = it },
+ label = { Text("Berat Badan (kg)") },
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ OutlinedTextField(
+ value = height,
+ onValueChange = { height = it },
+ label = { Text("Tinggi Badan (cm)") },
+ modifier = Modifier.fillMaxWidth()
+ )
+ } else {
+ // USC System
+ OutlinedTextField(
+ value = weight,
+ onValueChange = { weight = it },
+ label = { Text("Berat Badan (lbs)") },
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ OutlinedTextField(
+ value = feet,
+ onValueChange = { feet = it },
+ label = { Text("Tinggi (feet)") },
+ modifier = Modifier.weight(1f)
+ )
+
+ OutlinedTextField(
+ value = inches,
+ onValueChange = { inches = it },
+ label = { Text("Tinggi (inch)") },
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Button(
+ onClick = {
+ if (unitSystem == "Metric") {
+ val w = weight.toDoubleOrNull() ?: 0.0
+ val h = (height.toDoubleOrNull() ?: 0.0) / 100.0
+ bmi = if (h > 0) round(w / (h * h) * 100) / 100.0 else 0.0
+ } else {
+ val w = weight.toDoubleOrNull() ?: 0.0
+ val f = feet.toDoubleOrNull() ?: 0.0
+ val i = inches.toDoubleOrNull() ?: 0.0
+ val totalInches = (f * 12) + i
+ // BMI = (weight in lbs / (height in inches)^2) * 703
+ bmi = if (totalInches > 0) round((w / (totalInches * totalInches)) * 703 * 100) / 100.0 else 0.0
+ }
+
+ when {
+ bmi < 18.5 -> {
+ category = "Kurus (Underweight)"
+ color = Color(0xFF42A5F5)
+ }
+ bmi < 25.0 -> {
+ category = "Normal"
+ color = Color(0xFF66BB6A)
+ }
+ bmi < 30.0 -> {
+ category = "Overweight"
+ color = Color(0xFFFFA726)
+ }
+ else -> {
+ category = "Obesitas"
+ color = Color(0xFFEF5350)
+ }
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color(0xFF6200EE)
+ )
+ ) {
+ Text("Hitung BMI", fontSize = 18.sp)
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ if (bmi > 0.0) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = color.copy(alpha = 0.2f)
+ )
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "BMI: $bmi",
+ fontSize = 28.sp,
+ fontWeight = FontWeight.Bold,
+ color = color
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = category,
+ fontSize = 22.sp,
+ fontWeight = FontWeight.Medium,
+ color = color
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun InfoPage(navController: NavHostController) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Informasi BMI") },
+ navigationIcon = {
+ IconButton(onClick = { navController.popBackStack() }) {
+ Icon(Icons.Default.ArrowBack, contentDescription = "Back", tint = Color.White)
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = Color(0xFF6200EE),
+ titleContentColor = Color.White
+ )
+ )
+ }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .padding(24.dp)
+ ) {
+ Text(
+ "Kategori BMI",
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color(0xFF6200EE)
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ BMIInfoCard("Kurus (Underweight)", "BMI < 18.5", Color(0xFF42A5F5))
+ Spacer(modifier = Modifier.height(12.dp))
+
+ BMIInfoCard("Normal", "BMI 18.5 - 24.9", Color(0xFF66BB6A))
+ Spacer(modifier = Modifier.height(12.dp))
+
+ BMIInfoCard("Overweight", "BMI 25.0 - 29.9", Color(0xFFFFA726))
+ Spacer(modifier = Modifier.height(12.dp))
+
+ BMIInfoCard("Obesitas", "BMI ≥ 30.0", Color(0xFFEF5350))
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(0xFFF5F5F5)
+ ),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ Text(
+ "Tentang BMI",
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ "Body Mass Index (BMI) adalah ukuran lemak tubuh berdasarkan tinggi dan berat badan. BMI adalah indikator yang baik untuk mengetahui apakah berat badan Anda ideal atau tidak.",
+ fontSize = 14.sp,
+ lineHeight = 20.sp
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun BMIInfoCard(title: String, range: String, color: Color) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = color.copy(alpha = 0.2f)
+ ),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ Text(
+ text = title,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ color = color
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = range,
+ fontSize = 14.sp,
+ color = color.copy(alpha = 0.8f)
+ )
+ }
+ }
+ }
+}
\ 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
deleted file mode 100644
index d8ccd56..0000000
--- a/app/src/main/java/com/example/tiptime/MainActivity.kt
+++ /dev/null
@@ -1,196 +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.
- */
-package com.example.tiptime
-
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-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.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.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-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
-
-class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- enableEdgeToEdge()
- super.onCreate(savedInstanceState)
- setContent {
- TipTimeTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- ) {
- TipTimeLayout()
- }
- }
- }
- }
-}
-
-@Composable
-fun TipTimeLayout() {
- var amountInput by remember { mutableStateOf("") }
- var tipInput by remember { mutableStateOf("") }
- var roundUp by remember { mutableStateOf(false) }
-
- val amount = amountInput.toDoubleOrNull() ?: 0.0
- val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
- val tip = calculateTip(amount, tipPercent, roundUp)
-
- Column(
- modifier = Modifier
- .statusBarsPadding()
- .padding(horizontal = 40.dp)
- .verticalScroll(rememberScrollState())
- .safeDrawingPadding(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- Text(
- text = stringResource(R.string.calculate_tip),
- modifier = Modifier
- .padding(bottom = 16.dp, top = 40.dp)
- .align(alignment = Alignment.Start)
- )
- EditNumberField(
- label = R.string.bill_amount,
- leadingIcon = R.drawable.money,
- 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.how_was_the_service,
- leadingIcon = R.drawable.percent,
- 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.tip_amount, tip),
- style = MaterialTheme.typography.displaySmall
- )
- Spacer(modifier = Modifier.height(150.dp))
- }
-}
-
-@Composable
-fun EditNumberField(
- @StringRes label: Int,
- @DrawableRes leadingIcon: Int,
- keyboardOptions: KeyboardOptions,
- value: String,
- onValueChanged: (String) -> Unit,
- modifier: Modifier = Modifier
-) {
- TextField(
- value = value,
- singleLine = true,
- leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
- modifier = modifier,
- onValueChange = onValueChanged,
- label = { Text(stringResource(label)) },
- keyboardOptions = keyboardOptions
- )
-}
-
-@Composable
-fun RoundTheTipRow(
- roundUp: Boolean,
- onRoundUpChanged: (Boolean) -> Unit,
- modifier: Modifier = Modifier
-) {
- Row(
- modifier = modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(text = stringResource(R.string.round_up_tip))
- Switch(
- modifier = Modifier
- .fillMaxWidth()
- .wrapContentWidth(Alignment.End),
- checked = roundUp,
- onCheckedChange = onRoundUpChanged
- )
- }
-}
-
-/**
- * Calculates the tip based on the user input and format the tip amount
- * according to the local currency.
- * Example would be "$10.00".
- */
-private fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
- var tip = tipPercent / 100 * amount
- if (roundUp) {
- tip = kotlin.math.ceil(tip)
- }
- return NumberFormat.getCurrencyInstance().format(tip)
-}
-
-@Preview(showBackground = true)
-@Composable
-fun TipTimeLayoutPreview() {
- TipTimeTheme {
- TipTimeLayout()
- }
-}
diff --git a/app/src/main/java/com/example/tiptime/ui/theme/Theme.kt b/app/src/main/java/com/example/tiptime/ui/theme/Theme.kt
deleted file mode 100644
index b8c9adb..0000000
--- a/app/src/main/java/com/example/tiptime/ui/theme/Theme.kt
+++ /dev/null
@@ -1,115 +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.
- */
-package com.example.tiptime.ui.theme
-
-import android.os.Build
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.LocalContext
-
-private val LightColorScheme = lightColorScheme(
- primary = md_theme_light_primary,
- onPrimary = md_theme_light_onPrimary,
- primaryContainer = md_theme_light_primaryContainer,
- onPrimaryContainer = md_theme_light_onPrimaryContainer,
- secondary = md_theme_light_secondary,
- onSecondary = md_theme_light_onSecondary,
- secondaryContainer = md_theme_light_secondaryContainer,
- onSecondaryContainer = md_theme_light_onSecondaryContainer,
- tertiary = md_theme_light_tertiary,
- onTertiary = md_theme_light_onTertiary,
- tertiaryContainer = md_theme_light_tertiaryContainer,
- onTertiaryContainer = md_theme_light_onTertiaryContainer,
- error = md_theme_light_error,
- errorContainer = md_theme_light_errorContainer,
- onError = md_theme_light_onError,
- onErrorContainer = md_theme_light_onErrorContainer,
- background = md_theme_light_background,
- onBackground = md_theme_light_onBackground,
- surface = md_theme_light_surface,
- onSurface = md_theme_light_onSurface,
- surfaceVariant = md_theme_light_surfaceVariant,
- onSurfaceVariant = md_theme_light_onSurfaceVariant,
- outline = md_theme_light_outline,
- inverseOnSurface = md_theme_light_inverseOnSurface,
- inverseSurface = md_theme_light_inverseSurface,
- inversePrimary = md_theme_light_inversePrimary,
- surfaceTint = md_theme_light_surfaceTint,
- outlineVariant = md_theme_light_outlineVariant,
- scrim = md_theme_light_scrim,
-)
-
-private val DarkColorScheme = darkColorScheme(
- primary = md_theme_dark_primary,
- onPrimary = md_theme_dark_onPrimary,
- primaryContainer = md_theme_dark_primaryContainer,
- onPrimaryContainer = md_theme_dark_onPrimaryContainer,
- secondary = md_theme_dark_secondary,
- onSecondary = md_theme_dark_onSecondary,
- secondaryContainer = md_theme_dark_secondaryContainer,
- onSecondaryContainer = md_theme_dark_onSecondaryContainer,
- tertiary = md_theme_dark_tertiary,
- onTertiary = md_theme_dark_onTertiary,
- tertiaryContainer = md_theme_dark_tertiaryContainer,
- onTertiaryContainer = md_theme_dark_onTertiaryContainer,
- error = md_theme_dark_error,
- errorContainer = md_theme_dark_errorContainer,
- onError = md_theme_dark_onError,
- onErrorContainer = md_theme_dark_onErrorContainer,
- background = md_theme_dark_background,
- onBackground = md_theme_dark_onBackground,
- surface = md_theme_dark_surface,
- onSurface = md_theme_dark_onSurface,
- surfaceVariant = md_theme_dark_surfaceVariant,
- onSurfaceVariant = md_theme_dark_onSurfaceVariant,
- outline = md_theme_dark_outline,
- inverseOnSurface = md_theme_dark_inverseOnSurface,
- inverseSurface = md_theme_dark_inverseSurface,
- inversePrimary = md_theme_dark_inversePrimary,
- surfaceTint = md_theme_dark_surfaceTint,
- outlineVariant = md_theme_dark_outlineVariant,
- scrim = md_theme_dark_scrim,
-)
-
-@Composable
-fun TipTimeTheme(
- darkTheme: Boolean = isSystemInDarkTheme(),
- // Dynamic color is available on Android 12+
- // Dynamic color in this app is turned off for learning purposes
- dynamicColor: Boolean = false,
- content: @Composable () -> Unit
-) {
- val colorScheme = when {
- dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
- val context = LocalContext.current
- if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
- }
-
- darkTheme -> DarkColorScheme
- else -> LightColorScheme
- }
-
- MaterialTheme(
- colorScheme = colorScheme,
- typography = Typography,
- content = content
- )
-}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 26d4260..1504f5d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -20,5 +20,7 @@
Bill Amount
Tip Percentage
Round up tip?
- Tip Amount: %s
+ Tip Amount: %1$s
+
+