Tugas 6 - Membuat Aplikasi Kalkulator Konversi Mata Uang

Membuat Aplikasi Kalkulator Konversi Mata Uang

Ricardo Supriyanto - 5025221218

Pada tugas pertemuan 6, saya diberikan instruksi untuk melakukan pengayaan dengan membuat aplikasi kalkulator konversi mata uang dengan menggunakan Kotlin. Project yang saya buat ini dapat melakukan konversi untuk total ada 10 mata uang yaitu:
  • IDR (Indonesian Rupiah)
  • USD (United States Dollar)
  • JPY (Japanese Yen)
  • EUR (Euro)
  • GBP (British Pound Sterling)
  • AUD (Australian Dollar)
  • SGD (Singapore Dollar)
  • CNY (Chinese Yuan)
  • SAR (Saudi Riyal)
  • MYR (Malaysian Ringgit) 
Untuk melakukan konversi nilai tukarnya saya masih membuatnya dalam bentuk static dengan acuan konversinya berdasarkan Kurs Transaksi Bank Indonesia. Berikut merupakan contoh penggunaan aplikasi kalkulator Konversi Mata Uang tersebut:

Kode Program Modifikasi:

class NumberFormatVisualTransformation(
    private val numberFormat: DecimalFormat = DecimalFormat("#,###")
) : VisualTransformation {
    override fun filter(text: AnnotatedString): TransformedText {
        val originalText = text.text
        if (originalText.isEmpty()) {
            return TransformedText(text, OffsetMapping.Identity)
        }
        val digitsOnly = originalText.filter { it.isDigit() }
        if (digitsOnly.isEmpty()) {
            return TransformedText(AnnotatedString(""), OffsetMapping.Identity)
        }
        if (digitsOnly == "0") {
            return TransformedText(AnnotatedString("0"), OffsetMapping.Identity)
        }

        val formattedText = try {
            numberFormat.format(digitsOnly.toLong())
        } catch (_: NumberFormatException) {
            return TransformedText(text, OffsetMapping.Identity)
        }
        val offsetMapping = object : OffsetMapping {
            override fun originalToTransformed(offset: Int): Int {
                val originalSub = digitsOnly.take(offset)
                val formattedSub = try {
                    if (originalSub.isEmpty()) "" else numberFormat.format(originalSub.toLong())
                } catch (_: NumberFormatException) { "" }
                val separatorCount = formattedSub.count { it == ',' }
                return offset + separatorCount
            }
            override fun transformedToOriginal(offset: Int): Int {
                val formattedSub = formattedText.take(offset)
                val separatorCount = formattedSub.count { it == ',' }
                return max(0, offset - separatorCount)
            }
        }
        return TransformedText(AnnotatedString(formattedText), offsetMapping)
    }
}


class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MoneyConverterTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    CurrencyConverterScreen(modifier = Modifier.padding(innerPadding))
                }
            }
        }
    }
}

val exchangeRates = mapOf(
    "IDR" to 1.0, "USD" to 16000.0, "JPY" to 105.0, "EUR" to 17400.0, "GBP" to 20100.0,
    "AUD" to 10600.0, "SGD" to 11800.0, "CNY" to 2225.0, "SAR" to 4250.0, "MYR" to 3375.0
).toSortedMap()
val currencies = exchangeRates.keys.toList()

val currencyNames = mapOf(
    "IDR" to "Indonesian Rupiah", "USD" to "United States Dollar", "JPY" to "Japanese Yen",
    "EUR" to "Euro", "GBP" to "British Pound Sterling", "AUD" to "Australian Dollar",
    "SGD" to "Singapore Dollar", "CNY" to "Chinese Yuan", "SAR" to "Saudi Riyal",
    "MYR" to "Malaysian Ringgit"
)


@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CurrencyConverterScreen(modifier: Modifier = Modifier) {

    var amountInput by remember { mutableStateOf("") }
    var selectedFromCurrency by remember { mutableStateOf(currencies.firstOrNull { it == "IDR" } ?: currencies.firstOrNull() ?: "") }
    var selectedToCurrency by remember { mutableStateOf(currencies.firstOrNull { it == "USD" } ?: currencies.getOrNull(1) ?: "") }
    var conversionResultText by remember { mutableStateOf<String?>(null) }
    var detailedConversionText by remember { mutableStateOf<String?>(null)}
    var isAmountError by remember { mutableStateOf(false) }
    var fromDropdownExpanded by remember { mutableStateOf(false) }
    var toDropdownExpanded by remember { mutableStateOf(false) }
    val numberFormatTransformation = remember { NumberFormatVisualTransformation() }

    fun performConversion() {
        isAmountError = false
        detailedConversionText = null
        val amount = amountInput.toDoubleOrNull()

        if (amount == null) {
            if (amountInput.isNotBlank()) {
                isAmountError = true
            }
            conversionResultText = null
            detailedConversionText = null
            return
        }
        if (amount < 0) {
            isAmountError = true
            conversionResultText = null
            detailedConversionText = null
            return
        }


        if (selectedFromCurrency.isBlank() || selectedToCurrency.isBlank()) {
            conversionResultText = "Select currencies"
            return
        }

        val rateFrom = exchangeRates[selectedFromCurrency]
        val rateTo = exchangeRates[selectedToCurrency]

        if (rateFrom != null && rateTo != null && rateFrom != 0.0) {
            val result = amount * (rateFrom / rateTo)
            val decimalFormatResult = DecimalFormat("#,##0.00####")
            conversionResultText = "${decimalFormatResult.format(result)} $selectedToCurrency"

            val oneUnitResult = 1.0 * (rateFrom / rateTo)
            val decimalFormatDetail = DecimalFormat("#,##0.##")
            detailedConversionText = "1 $selectedFromCurrency = ${decimalFormatDetail.format(oneUnitResult)} $selectedToCurrency"

        } else {
            conversionResultText = "Error: Invalid rate"
            detailedConversionText = null
        }
    }

    Column(
        modifier = modifier
            .fillMaxSize()
            .background(Color(0xFFE0F7FA))
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            "Currency Converter",
            style = MaterialTheme.typography.headlineMedium,
            modifier = Modifier.padding(bottom = 24.dp),
            color = Color.Black
        )

        Card(
            modifier = Modifier.fillMaxWidth(),
            elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
            colors = CardDefaults.cardColors(containerColor = Color.White)
        ) {
            Column(
                modifier = Modifier.padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(16.dp)
            ) {
                OutlinedTextField(
                    value = amountInput,
                    onValueChange = { newValue ->
                        val digitsOnly = newValue.filter { it.isDigit() }
                        if (digitsOnly.length <= 15) {
                            if (digitsOnly.startsWith("0") && digitsOnly.length > 1) {
                                amountInput = ""
                                isAmountError = false
                                conversionResultText = null
                                return@OutlinedTextField
                            }
                            amountInput = digitsOnly
                            isAmountError = false
                            conversionResultText = null
                        }
                    },
                    label = { Text("Amount") },
                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
                    singleLine = true,
                    modifier = Modifier.fillMaxWidth(),
                    isError = isAmountError,
                    supportingText = {
                        if (isAmountError) {
                            Text("Please enter a valid number", color = MaterialTheme.colorScheme.error)
                        }
                    },
                    visualTransformation = numberFormatTransformation
                )

                ExposedDropdownMenuBox(
                    expanded = fromDropdownExpanded,
                    onExpandedChange = { fromDropdownExpanded = !fromDropdownExpanded }
                ) {
                    OutlinedTextField(
                        value = selectedFromCurrency,
                        onValueChange = {},
                        label = { Text("From Currency") },
                        readOnly = true,
                        trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = fromDropdownExpanded) },
                        modifier = Modifier.menuAnchor().fillMaxWidth()
                    )
                    ExposedDropdownMenu(
                        expanded = fromDropdownExpanded,
                        onDismissRequest = { fromDropdownExpanded = false }
                    ) {
                        currencies.forEach { currencyCode ->
                            val fullName = currencyNames[currencyCode] ?: currencyCode
                            DropdownMenuItem(
                                text = { Text("$currencyCode - $fullName") },
                                onClick = {
                                    selectedFromCurrency = currencyCode
                                    fromDropdownExpanded = false
                                }
                            )
                        }
                    }
                }

                IconButton(
                    onClick = {
                        val temp = selectedFromCurrency
                        selectedFromCurrency = selectedToCurrency
                        selectedToCurrency = temp
                    },
                    modifier = Modifier.align(Alignment.CenterHorizontally)
                ) {
                    Icon(Icons.Filled.SwapVert, contentDescription = "Swap Currencies")
                }

                ExposedDropdownMenuBox(
                    expanded = toDropdownExpanded,
                    onExpandedChange = { toDropdownExpanded = !toDropdownExpanded }
                ) {
                    OutlinedTextField(
                        value = selectedToCurrency,
                        onValueChange = {},
                        label = { Text("To Currency") },
                        readOnly = true,
                        trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = toDropdownExpanded) },
                        modifier = Modifier.menuAnchor().fillMaxWidth()
                    )
                    ExposedDropdownMenu(
                        expanded = toDropdownExpanded,
                        onDismissRequest = { toDropdownExpanded = false }
                    ) {
                        currencies.forEach { currencyCode ->
                            val fullName = currencyNames[currencyCode] ?: currencyCode
                            DropdownMenuItem(
                                text = { Text("$currencyCode - $fullName") },
                                onClick = {
                                    selectedToCurrency = currencyCode
                                    toDropdownExpanded = false
                                }
                            )
                        }
                    }
                }
            }
        }

        Button(
            onClick = { performConversion() },
            modifier = Modifier
                .fillMaxWidth(0.6f)
                .padding(top = 24.dp),
            enabled = amountInput.isNotBlank()
        ) {
            Text("Convert")
        }


        if (conversionResultText != null) {
            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(top = 16.dp),
                elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
                colors = CardDefaults.cardColors(containerColor = Color.White)
            ) {
                Column(
                    modifier = Modifier
                        .padding(16.dp)
                        .fillMaxWidth(),
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    Text(
                        text = conversionResultText!!,
                        style = MaterialTheme.typography.headlineSmall.copy(
                            fontWeight = FontWeight.Bold,
                        ),
                        textAlign = TextAlign.Center,
                        color = Color.Black
                    )
                    if (detailedConversionText != null) {
                        Spacer(modifier = Modifier.height(4.dp))
                        Text(
                            text = detailedConversionText!!,
                            style = MaterialTheme.typography.bodyMedium,
                            color = Color.Gray,
                            textAlign = TextAlign.Center
                        )
                    }
                }
            }
        }
    }
}

@Preview(showBackground = true, showSystemUi = true)
@Composable
fun CurrencyConverterScreenFinalPreview() {
    MoneyConverterTheme {
        Surface(modifier = Modifier.fillMaxSize()) {
            CurrencyConverterScreen()
        }
    }
}

Link Repository Github: Tugas 6 - PPB

Demo Aplikasi:



Referensi:

https://www.bi.go.id/id/statistik/informasi-kurs/transaksi-bi/default.aspx

Komentar

Postingan populer dari blog ini

Tugas 3 - Membangun Aplikasi Sederhana dengan Composable Teks

EAS - NewsApp

Tugas 11 - Membuat Aplikasi Authentication