Bignum & Formatting
As an application that handles financial data, ensuring numerical accuracy is paramount. To prevent floating-point inaccuracies, the app strictly avoids using Double
or Float
for any calculations involving monetary values.
The BigDecimal
Implementation
All financial data, including token balances, prices, and historical changes, is represented using BigDecimal
. This aligns with the backend API, which also provides financial data in a precise decimal format.
Finding a well-maintained Kotlin Multiplatform library for BigDecimal
was a challenge. After research, the project adopted com.ionspin.kotlin:bignum
. This library provides a reliable, cross-platform implementation of BigDecimal
and, crucially, includes a serialization module (bignum-serialization
) that integrates seamlessly with kotlinx.serialization
for network operations.
Centralized Formatting
To ensure consistency and maintainability, all logic for formatting BigDecimal
values for display is centralized. This is achieved using Kotlin Multiplatform's expect
/actual
mechanism, which defines a common formatting API in commonMain
while allowing for platform-specific implementations.
expect
Declarations (in commonMain
)
A set of expect
functions defines the public API for formatting numbers. This allows any ViewModel
or Composable in the shared codebase to format numbers without needing to know the underlying platform-specific details.
// commonMain/utils/Formatter.kt
// Formats a quote currency amount (e.g., $1,234.56)
expect fun formatQuote(amount: BigDecimal, showDecimal: Boolean = true): String
// Formats the change in a quote currency (e.g., +$12.34)
expect fun formatQuoteChange(amount: BigDecimal, showDecimal: Boolean = true): String
// Formats a balance of token (e.g., 1.524 ETH)
expect fun formatBalanceNative(amount: BigDecimal, symbol: String): String
// Formats a percentage value (e.g., +5.21%)
expect fun formatPercentage(amount: BigDecimal): String
actual
Implementations
Each platform provides its own actual
implementation of these functions, using the best tools available on that platform.
Android Implementation
On Android, the actual
functions use java.text.DecimalFormat
, which offers powerful, pattern-based formatting. The logic dynamically selects the appropriate pattern based on the magnitude of the number.
// androidMain/utils/Formatter.android.kt
actual fun formatQuote(amount: BigDecimal, showDecimal: Boolean): String {
val amountJvmBd = JvmBigDecimal(amount.toString())
val pattern = when {
showDecimal.not() -> "#,###"
amountJvmBd >= JvmBigDecimal.ONE -> "#,###.00"
amountJvmBd >= JvmBigDecimal("0.01") -> "0.00"
amountJvmBd.compareTo(JvmBigDecimal.ZERO) == 0 -> "0.00"
else -> "0.00000000"
}
val formatter = DecimalFormat(pattern)
return "$" + formatter.format(amountJvmBd)
}
iOS Implementation
On iOS, the actual
functions leverage the native NSNumberFormatter
. This ensures that all numbers are formatted according to the user's specific locale and settings on their device, providing a more natural and platform-idiomatic experience.
// iosMain/utils/Formatter.ios.kt
actual fun formatQuote(amount: BigDecimal, showDecimal: Boolean): String {
val formatter = NSNumberFormatter().apply {
numberStyle = NSNumberFormatterDecimalStyle
when {
!showDecimal -> {
minimumFractionDigits = 0uL
maximumFractionDigits = 0uL
}
amount >= 0.01 || amount.compareTo(BigDecimal.ZERO) == 0 -> {
minimumFractionDigits = 2uL
maximumFractionDigits = 2uL
}
else -> {
minimumFractionDigits = 8uL
maximumFractionDigits = 8uL
}
}
}
val decimalNumber = NSDecimalNumber(string = amount.toString())
return formatter.stringFromNumber(decimalNumber)?.let { "$$it" } ?: ""
}
This approach provides the best of both worlds: the guaranteed precision of BigDecimal
for all calculations and the best possible platform-native user experience for displaying data.