jetpack_compose_country_code_picker_emoji
jetpack_compose_country_code_picker_emoji copied to clipboard
Visual transformation causing the app to crash on compose v1.7.2
Describe the bug Recently, entering phone number to the text field is crashing the app.
For example: I want to enter a Canadian phone number +14165550000. The code will crash as soon as I've entered "4165550" into the text field.
Important note: The code runs well on its own, without any extra dependencies or dependency version upgrades. Then I integrated the library to my project, updated dependency versions and made some necessary edits, it's when the issue arise. The visual transformation logic no longer works in a reliable manner, crashing the app as offset mapping is now flawed.
Refer to my Github fork commits to see the steps I made to edit visual transformation logic to ensure the app doesn't crash.
To Reproduce Steps to reproduce the behavior:
- Tap the country flag to pick Canada
- Tap the text field to start entering your phone number
- Type
4165550 - Type the next number and observe the app crashes
Expected behavior The app shouldn't have crashed, but instead handle visually transforming the input as before.
Smartphone (please complete the following information):
- Device: Pixel 8a
- OS: Android 14
Additional context Refer to the stack trace error below
FATAL EXCEPTION: main (Ask Gemini)
Process: com.togitech.togii, PID: 15552
java.lang.IllegalStateException: OffsetMapping.transformedToOriginal returned invalid mapping: 0 -> -1 is not in range of original text [0, 8]
at androidx.compose.foundation.text.ValidatingOffsetMappingKt.validateTransformedToOriginal(ValidatingOffsetMapping.kt:116)
at androidx.compose.foundation.text.ValidatingOffsetMappingKt.throwIfNotValidTransform(ValidatingOffsetMapping.kt:73)
at androidx.compose.foundation.text.ValidatingOffsetMappingKt.throwIfNotValidTransform$default(ValidatingOffsetMapping.kt:60)
at androidx.compose.foundation.text.ValidatingOffsetMappingKt.filterWithValidation(ValidatingOffsetMapping.kt:35)
at androidx.compose.foundation.text.CoreTextFieldKt.CoreTextField(CoreTextField.kt:246)
at androidx.compose.foundation.text.BasicTextFieldKt.BasicTextField(BasicTextField.kt:765)
at androidx.compose.material.OutlinedTextFieldKt.OutlinedTextField(OutlinedTextField.kt:379)
at com.togitech.ccp.component.TogiCountryCodePickerKt.TogiCountryCodePicker(TogiCountryCodePicker.kt:173)
at com.togitech.togii.MainActivityKt.CountryCodePick(MainActivity.kt:77)
at com.togitech.togii.MainActivityKt$CountryCodePick$2.invoke(Unknown Source:8)
at com.togitech.togii.MainActivityKt$CountryCodePick$2.invoke(Unknown Source:10)
at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:192)
at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2825)
at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:3116)
at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3607)
at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:3552)
at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:948)
at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:1206)
at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:132)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:616)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:585)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:109)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:69)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1337)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1348)
at android.view.Choreographer.doCallbacks(Choreographer.java:952)
at android.view.Choreographer.doFrame(Choreographer.java:878)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1322)
at android.os.Handler.handleCallback(Handler.java:958)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8177)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.runtime.PausableMonotonicFrameClock@53d2e8, androidx.compose.ui.platform.MotionDurationScaleImpl@563ad01, StandaloneCoroutine{Cancelling}@9ed27a6, AndroidUiDispatcher@c72a5e7]
At the moment, I'm using 0 as the fallback index to reformat the input text to not crash the app, as follow:
// PhoneNumberTransformation.kt
@Suppress("AvoidMutableCollections", "AvoidVarsExceptWithDelegate")
private fun reformat(s: CharSequence, cursor: Int): Transformation {
phoneNumberFormatter.clear()
val curIndex = cursor - 1
var formatted: String? = null
var lastNonSeparator = 0.toChar()
var hasCursor = false
s.forEachIndexed { index, char ->
if (PhoneNumberUtils.isNonSeparator(char)) {
if (lastNonSeparator.code != 0) {
formatted = getFormattedNumber(lastNonSeparator, hasCursor)
hasCursor = false
}
lastNonSeparator = char
}
if (index == curIndex) {
hasCursor = true
}
}
if (lastNonSeparator.code != 0) {
formatted = getFormattedNumber(lastNonSeparator, hasCursor)
}
val originalToTransformed = mutableListOf<Int>()
val transformedToOriginal = mutableListOf<Int>()
var specialCharsCount = 0
formatted?.forEachIndexed { index, char ->
if (!PhoneNumberUtils.isNonSeparator(char)) {
specialCharsCount++
} else {
originalToTransformed.add(index)
}
// transformedToOriginal.add(index - specialCharsCount) // Disabled this line
transformedToOriginal.add(maxOf(index - specialCharsCount, 0)) // -> Changed to this line
}
originalToTransformed.add(originalToTransformed.maxOrNull()?.plus(1) ?: 0)
transformedToOriginal.add(transformedToOriginal.maxOrNull()?.plus(1) ?: 0)
return Transformation(formatted, originalToTransformed, transformedToOriginal)
}