Tugas 6 (Membuat Kalkulator Konversi Mata Uang)
Kalkulator Konversi Mata Uang
Pada kesempatan kali ini, di dapatkan tugas untuk membuat sebuah kalkulator konversi mata uang dengan menggunakan bahasa kotlin
Dalam artikel kali ini, akan saya tampilkan sebuah kalkulator biasa dan kalkulator konversi mata uang
Berikut hasil dari kalkulatornya
Tampilan Kalkulator Biasa
Tampilan Kalkulator Konversi Mata Uang
Readable Source Code
package com.zienzidan.calculator
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.zienzidan.calculator.ui.theme.CalculatorTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CalculatorTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier
.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
CalculatorApp()
}
}
}
}
}
Kode ini adalah entry point aplikasi kalkulator Android berbasis Jetpack Compose. Saat aplikasi dijalankan:
-
MainActivitydipanggil. -
UI disusun menggunakan Jetpack Compose dalam
setContent. -
Tema dan tampilan latar diterapkan dengan
CalculatorThemedanSurface. -
Fungsi
CalculatorApp()akan menjalankan logika dan UI utama dari kalkulator tersebut.
package com.zienzidan.calculator
import android.annotation.SuppressLint
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Calculate
import androidx.compose.material.icons.filled.Money
import androidx.compose.material.icons.outlined.Calculate
import androidx.compose.material.icons.outlined.Money
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.zienzidan.calculator.data.DataSource
data class TabBarItem(
val title: String,
val selectedIcon: ImageVector,
val unselectedIcon: ImageVector
)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "SuspiciousIndentation")
@Composable
fun CalculatorApp() {
val dataSource = DataSource()
val context = LocalContext.current
val calculatorTab = TabBarItem(
title = stringResource(id = R.string.app_name),
selectedIcon = Icons.Filled.Calculate,
unselectedIcon = Icons.Outlined.Calculate
)
val converterTab = TabBarItem(
title = stringResource(id = R.string.currency_converter),
selectedIcon = Icons.Filled.Money,
unselectedIcon = Icons.Outlined.Money
)
val tabBarItems = listOf(calculatorTab, converterTab)
//Create NavController
val navController = rememberNavController()
//Bottom Navigation Bar
Scaffold(
bottomBar = {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
tabBarItems.forEach { tabBarItem ->
NavigationBarItem(
icon = {
TabBarIconView(
isSelected = currentDestination?.hierarchy?.any { it.route == tabBarItem.title } == true,
selectedIcon = tabBarItem.selectedIcon,
unselectedIcon = tabBarItem.unselectedIcon,
title = tabBarItem.title
)
},
label = { Text(tabBarItem.title) },
selected = currentDestination?.hierarchy?.any { it.route == tabBarItem.title } == true,
onClick = {
navController.navigate(tabBarItem.title) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) {
NavHost(
navController = navController,
startDestination = calculatorTab.title
)
{
composable(calculatorTab.title) {
CalculatorScreen()
}
composable(converterTab.title) {
ConverterScreen(dataSource, context)
}
}
}
}
@Composable
fun TabBarIconView(
isSelected: Boolean,
selectedIcon: ImageVector,
unselectedIcon: ImageVector,
title: String
) {
Icon(
imageVector = if (isSelected) {
selectedIcon
} else {
unselectedIcon
},
contentDescription = title
)
}
@Preview(showSystemUi = true)
@Composable
fun AppPre() {
CalculatorApp()
}
Menampilkan dua tab: Kalkulator dan Konverter.
-
Menampilkan ikon dan label di bottom navigation.
-
Mengatur perpindahan antar tab saat diklik.
-
Menampilkan konten yang sesuai berdasarkan tab yang aktif.
Sederhananya, CalculatorApp() = Navigasi + UI utama + Tab bar aplikasi
package com.zienzidan.calculator
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Backspace
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
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.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.github.murzagalin.evaluator.Evaluator
val evaluator = Evaluator()
@Composable
fun CalculatorScreen() {
var expression by rememberSaveable {
mutableStateOf("")
}
val expressionColorNormal = MaterialTheme.colorScheme.onSurface
val expressionColorError = Color.Red
var expressionColor by remember { mutableStateOf(expressionColorNormal) }
Column (
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(8.dp, 8.dp, 8.dp, 85.dp)
) {
Column(
modifier = Modifier
) {
ElevatedCard {
Text(
text = expression,
modifier = Modifier
.fillMaxWidth(),
lineHeight = 90.sp,
textAlign = TextAlign.End,
fontSize = 80.sp,
maxLines = 4,
overflow = TextOverflow.Ellipsis,
color = expressionColor
)
}
}
Column(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.fillMaxWidth()
.fillMaxSize()
.weight(0.5f),
verticalArrangement = Arrangement.Bottom
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression = ""
expressionColor = expressionColorNormal
}
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = stringResource(R.string.reset)
)
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression += "("
expressionColor = expressionColorNormal
}
) {
Text(text = "(")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression += ")"
expressionColor = expressionColorNormal
}
) {
Text(text = ")")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression = addTheOperant(expression,"/")
expressionColor = expressionColorNormal
}
) {
Text(text = "/")
}
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression += "7"
expressionColor = expressionColorNormal
}
) {
Text(text = "7")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression += "8"
expressionColor = expressionColorNormal
}
) {
Text(text = "8")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression += "9"
expressionColor = expressionColorNormal
}
) {
Text(text = "9")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression = addTheOperant(expression,"*")
expressionColor = expressionColorNormal
}
) {
Text(text = "*")
}
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression += "4"
expressionColor = expressionColorNormal
}
) {
Text(text = "4")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression += "5"
expressionColor = expressionColorNormal
}
) {
Text(text = "5")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression += "6"
expressionColor = expressionColorNormal
}
) {
Text(text = "6")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression = addTheOperant(expression,"-")
expressionColor = expressionColorNormal
}
) {
Text(text = "-")
}
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression += "1"
expressionColor = expressionColorNormal
}
) {
Text(text = "1")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression += "2"
expressionColor = expressionColorNormal
}
) {
Text(text = "2")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression += "3"
expressionColor = expressionColorNormal
}
) {
Text(text = "3")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression = addTheOperant(expression,"+")
expressionColor = expressionColorNormal
}
) {
Text(text = "+")
}
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression += "0"
expressionColor = expressionColorNormal
}
) {
Text(text = "0")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp)
,
onClick = {
expression = addTheOperant(expression,".")
expressionColor = expressionColorNormal
}
) {
Text(text = ".")
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
expression = expression.dropLast(1)
expressionColor = expressionColorNormal
}
) {
Icon(
imageVector = Icons.AutoMirrored.Default.Backspace,
contentDescription = stringResource(R.string.backspace)
)
}
Button(
modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
try {
expression = evaluateExpression(expression)
expressionColor = expressionColorNormal
}catch (ex: Exception){
expressionColor = expressionColorError
}
}
) {
Text(text = "=")
}
}
}
}
}
//Checks if expression already ends with an operant before adding a new one
fun addTheOperant(expression: String, operant: String): String{
return if (expression.endsWith("+") ||
expression.endsWith("-") ||
expression.endsWith("*") ||
expression.endsWith("/") ||
expression.endsWith(".")){
expression
} else{
expression+operant
}
}
//Uses the Evaluator library to evaluate the expression into a result
fun evaluateExpression(expression: String): String {
return try {
val result = evaluator.evaluateDouble(expression).toString()
if (result.endsWith(".0")){
result.dropLast(2)
} else{
result
}
} catch (ex: Exception){
throw Exception("Wrong Expression Format")
}
}
@Preview(showSystemUi = true)
@Composable
fun CalculatorScreenPr(){
CalculatorScreen()
}
Kode tersebut membuat aplikasi kalkulator di Android menggunakan Jetpack Compose yang bisa:
-
Menampilkan dan mengedit ekspresi matematika.
-
Mendukung operasi dasar:
+,-,*,/,(), dan.. -
Menghapus karakter (
Backspace) dan mereset ekspresi (Refresh). -
Menghitung hasil saat tombol
=ditekan menggunakan libraryEvaluator. -
Menampilkan kesalahan dengan warna merah jika ekspresi salah.
Semuanya ditampilkan dalam antarmuka tombol interaktif
package com.zienzidan.calculator
import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Backspace
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.zienzidan.calculator.data.ApiClient
import com.zienzidan.calculator.data.ConvertionResult
import com.zienzidan.calculator.data.DataSource
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConverterScreen(dataSource: DataSource, context: Context) {
val currencyList = dataSource.currencyList
var expandedFrom by remember { mutableStateOf(false) }
var selectedFromOption by rememberSaveable { mutableStateOf(currencyList[0].code) }
var symbolFrom by rememberSaveable { mutableStateOf(currencyList[0].symbol) }
var expandedTo by remember { mutableStateOf(false) }
var selectedToOption by rememberSaveable { mutableStateOf(currencyList[1].code) }
var symbolTo by rememberSaveable { mutableStateOf(currencyList[1].symbol) }
var amount by rememberSaveable { mutableStateOf("") }
var result by rememberSaveable { mutableStateOf("") }
var loadingIndication by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(8.dp, 8.dp, 8.dp, 85.dp)
) {
Column(
modifier = Modifier
) {
ElevatedCard {
Text(
text = symbolFrom + amount,
modifier = Modifier.fillMaxWidth(),
lineHeight = 90.sp,
textAlign = TextAlign.End,
fontSize = 80.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
Column(
modifier = Modifier.padding(2.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(2.dp)
) {
Text(
text = stringResource(R.string.from),
style = MaterialTheme.typography.displaySmall,
modifier = Modifier.weight(0.2f)
)
ExposedDropdownMenuBox(
expanded = expandedFrom,
onExpandedChange = {
expandedFrom = !expandedFrom
}) {
TextField(
value = selectedFromOption,
onValueChange = {},
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedFrom) },
modifier = Modifier.menuAnchor(),
textStyle = TextStyle.Default
)
ExposedDropdownMenu(
expanded = expandedFrom,
onDismissRequest = { expandedFrom = false }) {
currencyList.forEach { item ->
DropdownMenuItem(
text = { Text(text = item.code + " : " + item.name) },
onClick = {
selectedFromOption = item.code
expandedFrom = false
symbolFrom = item.symbol
result = ""
})
}
}
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(2.dp)
) {
Text(
text = stringResource(R.string.to),
style = MaterialTheme.typography.displaySmall,
modifier = Modifier.weight(0.2f)
)
ExposedDropdownMenuBox(
expanded = expandedTo,
onExpandedChange = {
expandedTo = !expandedTo
}) {
TextField(
value = selectedToOption,
onValueChange = {},
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedTo) },
modifier = Modifier.menuAnchor(),
textStyle = TextStyle.Default
)
ExposedDropdownMenu(
expanded = expandedTo,
onDismissRequest = { expandedTo = false }) {
currencyList.forEach { item ->
DropdownMenuItem(
text = { Text(text = item.code + " : " + item.name) },
onClick = {
selectedToOption = item.code
expandedTo = false
symbolTo = item.symbol
result = ""
})
}
}
}
}
}
Column(
modifier = Modifier
) {
ElevatedCard {
Text(
text = symbolTo + result,
modifier = Modifier.fillMaxWidth(),
lineHeight = 90.sp,
textAlign = TextAlign.End,
fontSize = 80.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
if (loadingIndication){
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(2.dp),
color = MaterialTheme.colorScheme.secondary,
trackColor = MaterialTheme.colorScheme.surfaceVariant,
)
}
Column(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.fillMaxWidth()
.weight(0.5f),
verticalArrangement = Arrangement.Bottom
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp), onClick = {
amount += "7"
}) {
Text(text = "7")
}
Button(modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp), onClick = {
amount += "8"
}) {
Text(text = "8")
}
Button(modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp), onClick = {
amount += "9"
}) {
Text(text = "9")
}
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp), onClick = {
amount += "4"
}) {
Text(text = "4")
}
Button(modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp), onClick = {
amount += "5"
}) {
Text(text = "5")
}
Button(modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp), onClick = {
amount += "6"
}) {
Text(text = "6")
}
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp), onClick = {
amount += "1"
}) {
Text(text = "1")
}
Button(modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp), onClick = {
amount += "2"
}) {
Text(text = "2")
}
Button(modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp), onClick = {
amount += "3"
}) {
Text(text = "3")
}
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp), onClick = {
amount += "0"
}) {
Text(text = "0")
}
Button(modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp), onClick = {
amount += "."
}
) {
Text(text = ".")
}
Button(modifier = Modifier
.weight(1f)
.height(60.dp)
.padding(2.dp),
onClick = {
amount = amount.dropLast(1)
result = ""
}) {
Icon(
imageVector = Icons.AutoMirrored.Default.Backspace,
contentDescription = stringResource(R.string.backspace)
)
}
}
Row {
Button(
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.padding(2.dp),
onClick = {
loadingIndication = true
if (selectedFromOption == selectedToOption || amount == "" || amount == "0") {
result = amount
} else {
val call = ApiClient.apiService.convertAmount(
amount,
selectedFromOption,
selectedToOption
)
//Calls the API to get the conversion
call.enqueue(object : Callback<ConvertionResult> {
override fun onResponse(
call: Call<ConvertionResult>,
response: Response<ConvertionResult>
) {
if (response.isSuccessful) {
val post = response.body()
result =
convertRateToValue(
post?.rates.toString(),
selectedToOption
)
} else {
Toast.makeText(
context,
context.getString(R.string.conversion_failed),
Toast.LENGTH_LONG
).show()
}
}
override fun onFailure(call: Call<ConvertionResult>, t: Throwable) {
Toast.makeText(
context,
context.getString(R.string.connection_error),
Toast.LENGTH_LONG
).show()
}
})
}
loadingIndication = false
}
) {
Text(text = stringResource(R.string.convert_currency))
}
}
}
}
}
//Converts the received rate from the api to only the value
private fun convertRateToValue(rate: String, selectedToOption: String): String {
val pattern = Regex("""$selectedToOption=(.*?)(?=,|$)""")
val match = pattern.find(rate)
return match?.groupValues?.get(1) ?: "0.0"
}
@Preview(showSystemUi = true)
@Composable
fun ConverterScreenPr() {
val dataSource = DataSource()
val context = LocalContext.current
ConverterScreen(dataSource, context)
}
kode di atas adalah layar konversi mata uang menggunakan Jetpack Compose di Android. Fitur utamanya:
-
Menampilkan nilai input dan hasil konversi dalam format mata uang.
-
Memilih mata uang asal dan tujuan dari dropdown.
-
Input angka lewat tombol seperti kalkulator.
-
Tombol "Convert Currency" menghubungi API untuk mengonversi jumlah.
-
Menampilkan progress bar saat loading.
-
Menangani error dengan
Toast.
Strukturnya menggunakan Column, Row, Button, dan TextField dalam UI Compose
Video Presentasi
Source Code : Kalkulator Konversi Mata Uang
Referensi
https://kuliahppb.blogspot.com/2025/04/penggunaan-kotlin-1.html
https://www.idn.id/mengapa-kotlin-menjadi-masa-depan-bagi-pemrograman-android/
Komentar
Posting Komentar