I have a custom view aiming to switch between two currencies. The idea is simple, a main text field with a currency abbreviation, a button to switch and a chip with the converted amount.
In the custom view, I have another custom view which contains an EditText
and a TextView
which is what I use for the main amount display. Here is a small diagram of the 'layout' of the views:
As you can see, there isn't a huge amount going on here in terms of components. However, internally, I am doing a certain amount of calculations and logic.
In the AmountEditText
view, I have a single custom attribute which sets the value of the TextView
component and the EditText
simply handles user input (limited to digits) and has a backing property converting the text entered to a Float
.
In the SwitchableTextField
(which extends a ConstraintLayout
), I have more custom attributes being the following:
TextView
in the AmountEditText
EditText
in the AmountEditText
to display in the Chip
The code I am having issues with is in the SwitchableTextField
and is the following (Some parts have been removed for simplicity's sake):
class SwitchableTextField(context: Context, attrs: AttributeSet) :
ConstraintLayout(context, attrs) {
var defaultCurrency: String = CURRENCY_EUR
set(value) {
if (field != value) {
field = value
binding.mainAmountField.currency = value
}
}
var mainAmount: Float = 0f
get() = binding.mainAmountField.tokenAmount.toString().parseDecimalNumber()
set(value) {
if (field != value) {
field = value
convertAmount(value)
}
}
var conversionCurrency: String = CURRENCY_SYMBOL
set(value) {
if (field != value) {
field = value
}
}
var convertedAmount: Float = 0f
var assetAmount: Float = 0f
var conversionDirection: Boolean = true
var listener: AmountInputListener? = null
init {
isSaveEnabled = true
applyUserAttributes(context, attrs)
binding.mainAmountField.currency = defaultCurrency
binding.mainAmountField.addOnTextChangedListener { text ->
mainAmount = text.parseDecimalNumber()
}
binding.currencySwitchButton.setOnClickListener {
switchConversionDirection()
}
}
private fun applyUserAttributes(context: Context, attrs: AttributeSet?) {
val typedArray = context.theme.obtainStyledAttributes(
attrs,
R.styleable.SwitchableCurrencyField,
0,
0
)
defaultCurrency = typedArray.getString(R.styleable.SwitchableCurrencyField_defaultCurrency)
?: defaultCurrency
conversionCurrency =
typedArray.getString(R.styleable.SwitchableCurrencyField_conversionCurrency)
?: conversionCurrency
conversionDirection =
typedArray.getBoolean(R.styleable.SwitchableCurrencyField_convertFromAsset, true)
typedArray.recycle()
}
// Some internal code
override fun onSaveInstanceState(): Parcelable? {
return SavedState(super.onSaveInstanceState()).apply {
childrenStates = saveChildViewStates()
}
}
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>?) {
dispatchFreezeSelfOnly(container)
}
internal class SavedState : AbsSavedState {
internal var childrenStates: SparseArray<Parcelable>? = null
constructor(superState: Parcelable?) : super(superState)
@Suppress("UNCHECKED_CAST")
constructor(source: Parcel) : super(source) {
childrenStates = source.readSparseArray(javaClass.classLoader)
}
@Suppress("UNCHECKED_CAST")
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeSparseArray(childrenStates as SparseArray<Any>)
}
companion object {
@Suppress("UNUSED")
@JvmField
val CREATOR = object : Parcelable.Creator<SavedState> {
override fun createFromParcel(source: Parcel) = SavedState(source)
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
}
}
}
public override fun onRestoreInstanceState(state: Parcelable?) {
when (state) {
is SavedState -> {
super.onRestoreInstanceState(state.superState)
state.childrenStates?.let { restoreChildViewStates(it) }
}
else -> super.onRestoreInstanceState(state)
}
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>?) {
dispatchThawSelfOnly(container)
}
}
Assume I have the following attributes set for the view:
Upon entering "10" in the EditText
, the view displays the following:
10 EUR
// Button
11.87 USD
Pressing the ImageButton works as intended and both the values and currency abbreviations switch. However, the issue I have is that when I navigate to the next fragment after having switched (meaning USD is in the AmountEditText
and not EUR anymore), returning to the fragment with this view keeps the amount in the EditText
but returns the defined attributes to the values they had originally: defaultCurrency = "EUR", conversionCurrency = "USD", conversionDirection = true
The workflow I expect to happen is the following:
10 EUR 11.87 USD 11.87 USD
// Button -- Switching currency --> // Button -- Next fragment then back --> // Button
11.87 USD 10 EUR 10 EUR
What actually happens is:
10 EUR 11.87 USD 11.87 EUR
// Button -- Switching currency --> // Button -- Next fragment then back --> // Button
11.87 USD 10 EUR 14.09 USD
Meaning the text entered in the EditText
stays (which is good) but the rest is reset and hence the conversion is incorrect.
I suspect the issue comes from the custom attributes being read from the TypedArray
and resetting the data restored but I have no idea how I should correct this.
The solution is rather simple but took me long enough to find.
Removing the if (field != value)
from the properties and correctly implementing the SavedState
with internal var ...
for each of the properties that require saving does the trick.
NOTE : The readBoolean
and writeBoolean
functions for a Parcel
exist only from API 29 so using another type was required (in my case a custom enum for the attributes resulting in an int).
Here is the resulting onSaveInstanceState
function from which you can deduce the remaining code in the other functions:
override fun onSaveInstanceState(): Parcelable? {
return SavedState(super.onSaveInstanceState()).apply {
childrenStates = saveChildViewStates()
defaultCurrency = this@SwitchableCurrencyField.defaultCurrency
conversionCurrency = this@SwitchableCurrencyField.conversionCurrency
conversionDirection = this@SwitchableCurrencyField.conversionDirection
mainAmount = this@SwitchableCurrencyField.mainAmount
}
}