Issue
I have a composable function that implements an OutlinedTextField and some other simple compose components. Recently, I have been facing a crash in app when I try to erase the text value pressing the backspace key from my real device. I also have tested in emulator and other smartphones.
To reproduce this error, When I enter this screen that has 2 EditTexts (EditText is the composable that implement an OutlinedTextField) and a Switch in bottom, in the CVV EditText component I type, for example, "123", then click on Switch, and then try to erase the text value from "CVV" field, the app crashes with this stack trace:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: air.br.com.alelo.mobile.android.hom, PID: 14127
java.lang.IllegalArgumentException: width and height must be > 0
at android.graphics.Bitmap.createBitmap(Bitmap.java:1102)
at androidx.compose.ui.graphics.Api26Bitmap$Companion.createBitmap-x__-hDU$ui_graphics_release(AndroidImageBitmap.android.kt:198)
at androidx.compose.ui.graphics.AndroidImageBitmap_androidKt.ActualImageBitmap-x__-hDU(AndroidImageBitmap.android.kt:43)
at androidx.compose.ui.graphics.ImageBitmapKt.ImageBitmap-x__-hDU(ImageBitmap.kt:253)
at androidx.compose.ui.graphics.ImageBitmapKt.ImageBitmap-x__-hDU$default(ImageBitmap.kt:247)
at androidx.compose.foundation.text.selection.AndroidSelectionHandles_androidKt.createHandleImage(AndroidSelectionHandles.android.kt:164)
at androidx.compose.foundation.text.AndroidCursorHandle_androidKt$drawCursorHandle$1$1.invoke(AndroidCursorHandle.android.kt:69)
at androidx.compose.foundation.text.AndroidCursorHandle_androidKt$drawCursorHandle$1$1.invoke(AndroidCursorHandle.android.kt:66)
at androidx.compose.ui.draw.DrawContentCacheModifier.onBuildCache(DrawModifier.kt:207)
at androidx.compose.ui.node.DrawEntity$updateCache$1.invoke(DrawEntity.kt:59)
at androidx.compose.ui.node.DrawEntity$updateCache$1.invoke(DrawEntity.kt:56)
at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:126)
at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:88)
at androidx.compose.ui.node.DrawEntity.draw(DrawEntity.kt:97)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:279)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:269)
at androidx.compose.ui.node.ModifiedLayoutNode.performDraw(ModifiedLayoutNode.kt:98)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:277)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:269)
at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.performDraw(DelegatingLayoutNodeWrapper.kt:69)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:277)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:269)
at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:868)
at androidx.compose.ui.node.InnerPlaceable.performDraw(InnerPlaceable.kt:140)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:277)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:269)
at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.performDraw(DelegatingLayoutNodeWrapper.kt:69)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:277)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:269)
at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.performDraw(DelegatingLayoutNodeWrapper.kt:69)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:277)
at androidx.compose.ui.node.LayoutNodeWrapper.access$drawContainedDrawModifiers(LayoutNodeWrapper.kt:59)
at androidx.compose.ui.node.LayoutNodeWrapper$invoke$1.invoke(LayoutNodeWrapper.kt:294)
at androidx.compose.ui.node.LayoutNodeWrapper$invoke$1.invoke(LayoutNodeWrapper.kt:293)
at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:1798)
at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:121)
at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:88)
at androidx.compose.ui.node.LayoutNodeWrapper.invoke(LayoutNodeWrapper.kt:293)
at androidx.compose.ui.node.LayoutNodeWrapper.invoke(LayoutNodeWrapper.kt:59)
at androidx.compose.ui.platform.RenderNodeApi29.record(RenderNodeApi29.android.kt:168)
at androidx.compose.ui.platform.RenderNodeLayer.updateDisplayList(RenderNodeLayer.android.kt:292)
E/AndroidRuntime: at androidx.compose.ui.platform.RenderNodeLayer.drawLayer(RenderNodeLayer.android.kt:234)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:264)
at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:868)
at androidx.compose.ui.node.InnerPlaceable.performDraw(InnerPlaceable.kt:140)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:277)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:269)
at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.performDraw(DelegatingLayoutNodeWrapper.kt:69)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:277)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:269)
at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.performDraw(DelegatingLayoutNodeWrapper.kt:69)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:277)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:269)
at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.performDraw(DelegatingLayoutNodeWrapper.kt:69)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:277)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:269)
at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.performDraw(DelegatingLayoutNodeWrapper.kt:69)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:277)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:269)
at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.performDraw(DelegatingLayoutNodeWrapper.kt:69)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:277)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:269)
at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.performDraw(DelegatingLayoutNodeWrapper.kt:69)
at androidx.compose.ui.node.LayoutNodeWrapper.drawContainedDrawModifiers(LayoutNodeWrapper.kt:277)
at androidx.compose.ui.node.LayoutNodeWrapper.draw(LayoutNodeWrapper.kt:269)
at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:868)
at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:852)
at android.view.View.draw(View.java:23901)
at android.view.View.updateDisplayListIfDirty(View.java:22767)
at android.view.View.draw(View.java:23628)
at android.view.ViewGroup.drawChild(ViewGroup.java:5355)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:5112)
at android.view.View.updateDisplayListIfDirty(View.java:22753)
at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:602)
at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:608)
at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:684)
at android.view.ViewRootImpl.draw(ViewRootImpl.java:5440)
at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:5148)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:4212)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2919)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:10491)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1108)
at android.view.Choreographer.doCallbacks(Choreographer.java:866)
at android.view.Choreographer.doFrame(Choreographer.java:797)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1092)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:226)
at android.os.Looper.loop(Looper.java:313)
at android.app.ActivityThread.main(ActivityThread.java:8669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)
E/AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1135)
My project contains only one activity and 1 screen.
@AndroidEntryPoint
class SelectCardsToUnlockActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ExpirationDateAndCvvScreen()
}
}
}
The Screen that launches this exception when I try erase text is the ExpirationDateAndCVVScreen.
@Composable
fun ExpirationDateAndCvvScreen(
viewModel: ExpirationDateAndCvvViewModel = hiltViewModel(),
) {
val expirationDateValue = viewModel.expirationDate.collectAsState().value
val expirationDateError = viewModel.expirationDateError.collectAsState().value
val cvv = viewModel.cvv.collectAsState().value
val tduAcceptanceValue = viewModel.tduAcceptance.collectAsState().value
Scaffold(
modifier = Modifier,
content = {
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.padding(start = Spacing_16DP, end = Spacing_16DP, bottom = Spacing_16DP)
.fillMaxSize()
.align(Alignment.TopCenter)
) {
FormForCvvAndDateExpiration(
modifier = Modifier.fillMaxWidth(),
expirationDateValue = expirationDateValue,
expirationDateError = expirationDateError,
cvvValue = cvv,
onExpirationDateValueChange = { expirationDate ->
viewModel.setExpirationDate(expirationDate = expirationDate)
},
onCvvValueChange = { cvv ->
viewModel.setCvv(cvv = cvv)
}
)
TDUAcceptanceSection(
modifier = Modifier
.padding(bottom = Spacing_64DP)
.fillMaxSize(),
acceptanceStatus = tduAcceptanceValue,
onAcceptanceChange = { isAccepted ->
viewModel.setTDUAcceptance(status = isAccepted)
}
)
}
}
}
)
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun TDUAcceptanceSection(
modifier: Modifier = Modifier,
acceptanceStatus: Boolean,
onAcceptanceChange: (status: Boolean) -> Unit
) {
Box(
modifier = modifier
) {
Row(
modifier = Modifier.align(Alignment.BottomStart),
horizontalArrangement = Arrangement.spacedBy(Spacing_22DP),
) {
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
Switch(
modifier = Modifier.padding(start = Spacing_4DP),
checked = acceptanceStatus,
onCheckedChange = { isAccepted ->
onAcceptanceChange(isAccepted)
}
)
}
}
}
}
@Composable
fun FormForCvvAndDateExpiration(
modifier: Modifier = Modifier,
expirationDateValue: String,
expirationDateError: Boolean,
cvvValue: String,
onExpirationDateValueChange: (value: String) -> Unit,
onCvvValueChange: (value: String) -> Unit
) {
val maskExpirationDate = remember {
mutableStateOf(ExpirationDateMaskDefaults.visualTransformation)
}
val maskCVV = remember {
mutableStateOf(VisualTransformation.None)
}
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(Spacing_16DP)
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(Spacing_16DP)
) {
EditText(
modifier = Modifier.weight(1f),
value = expirationDateValue,
title = "Expiration date",
onValueChange = {
onExpirationDateValueChange(it)
},
error = if (!expirationDateError) "" else "Type a valid date",
maxLength = 4,
visualTransformation = maskExpirationDate.value,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
EditText(
modifier = Modifier.weight(1f),
value = cvvValue,
title = "CVV",
onValueChange = {
onCvvValueChange(it)
},
error = "",
maxLength = 3,
visualTransformation = maskCVV.value,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
}
}
}
My ViewModel that controls the screen is very simple.
@HiltViewModel
class ExpirationDateAndCvvViewModel @Inject constructor(
) : ViewModel() {
private val _expirationDate = MutableStateFlow("")
val expirationDate = _expirationDate.asStateFlow()
private val _cvv = MutableStateFlow("")
val cvv = _cvv.asStateFlow()
private val _expirationDateError = MutableStateFlow(false)
val expirationDateError = _expirationDateError.asStateFlow()
private val _tduAcceptance = MutableStateFlow(false)
val tduAcceptance = _tduAcceptance.asStateFlow()
fun setExpirationDate(expirationDate: String) {
_expirationDate.value = expirationDate
_expirationDateError.value = isExpirationDateInvalid(date = expirationDate)
}
fun setCvv(cvv: String) {
_cvv.value = cvv
}
fun setTDUAcceptance(status: Boolean) {
_tduAcceptance.value = status
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun isExpirationDateInvalid(
date: String,
todayCalendar: Calendar = Calendar.getInstance()
): Boolean {
if (date.length == 4) {
val dateMonth = date.substring(range = IntRange(0, 1)).toInt()
if (dateMonth !in 1..12)
return true
val dateYear = date.substring(range = IntRange(2, 3)).toInt() + 2000
val todayMonth = todayCalendar.get(Calendar.MONTH) + 1
val todayYear = todayCalendar.get(Calendar.YEAR)
if (dateYear < todayYear) return true
if (dateYear == todayYear && dateMonth < todayMonth) return true
}
return false
}
}
and the code for my composable EditText is
@Composable
fun EditText(
modifier: Modifier = Modifier,
value: String,
title: String? = null,
error: String = "",
enabled: Boolean = true,
readOnly: Boolean = false,
onValueChange: (String) -> Unit = {},
maxLength: Int = -1,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
visualTransformation: VisualTransformation = VisualTransformation.None,
) {
Column(
modifier = modifier
) {
title?.let {
Text(
text = title,
color = Gray69,
style = Body3_500,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = Spacing_4DP)
)
}
OutlinedTextField(
value = value,
onValueChange = { textInput ->
if (maxLength > -1 && textInput.length <= maxLength) onValueChange(textInput)
},
enabled = enabled,
readOnly = readOnly,
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = GrayA5,
unfocusedBorderColor =
error.let {
if (it.isNotEmpty()) Red
else GrayD7
}
),
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = visualTransformation,
)
Text(
text = error,
style = Body3_500,
color = Red,
modifier = Modifier
.fillMaxWidth()
.padding(top = Spacing_4DP)
)
}
}
and my compose dependencies are:
composeDeps = { dependencyHandler ->
delegate = dependencyHandler
dependencies {
implementation "androidx.compose.ui:ui:1.1.0"
implementation "androidx.compose.ui:ui-tooling-preview:1.1.1"
implementation "androidx.compose.material:material:1.1.0"
implementation "androidx.compose.foundation:foundation:1.1.0"
implementation "androidx.compose.material:material-icons-core:1.1.0"
implementation "androidx.compose.material:material-icons-extended:1.1.0"
implementation "androidx.activity:activity-compose:1.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
implementation "androidx.compose.runtime:runtime-livedata:1.1.0"
implementation "meu.alelo.compose_component:compose_components:0.1.9-beta.11"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.0.5"
debugImplementation "androidx.compose.ui:ui-tooling:1.1.0"
debugImplementation "androidx.compose.ui:ui-test-manifest:1.1.1"
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"
}
}
The OutlinedTextField's use masks for date in the format MM/YY.
The code for mask is
class MaskVisualTransformation(private val mask: String) : VisualTransformation {
private val specialSymbolsIndices = mask.indices.filter { mask[it] != '#' }
override fun filter(text: AnnotatedString): TransformedText {
var out = ""
var maskIndex = 0
text.forEach { char ->
while (specialSymbolsIndices.contains(maskIndex)) {
out += mask[maskIndex]
maskIndex++
}
out += char
maskIndex++
}
return TransformedText(AnnotatedString(out), offsetTranslator())
}
private fun offsetTranslator() = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
val offsetValue = offset.absoluteValue
if (offsetValue == 0) return 0
var numberOfHashtags = 0
val masked = mask.takeWhile {
if (it == '#') numberOfHashtags++
numberOfHashtags < offsetValue
}
return masked.length + 1
}
override fun transformedToOriginal(offset: Int): Int {
return mask.take(offset.absoluteValue).count { it == '#' }
}
}
}
object ExpirationDateMaskDefaults {
private const val MASK = "##/##"
private const val INPUT_LENGTH = 4
val visualTransformation = MaskVisualTransformation(MASK)
fun onValueChange(value: String, onValueChange: (String) -> Unit) {
val valueFilter = value.filter { charDig ->
charDig.isDigit()
}
if (valueFilter.length <= INPUT_LENGTH) {
onValueChange.invoke(valueFilter)
}
}
}
The most strange in this error is that If put some text in the OutlinedTextField and erase after, the crash doesn't happen. But if I put some text, click in the Switch or other component, like a ClickableText That I have also, and then try to erase the text value the app crashes with that stack trace.
I have been looking for answers since last week and I have found nothing about it. The closest solution I found is this one, but the stack trace is not the same as the one I have, but when I read I thought I could try, because these OutlinedTextFields that I implemented use some Masks (Visual Transformation).
If someone could take a look, I would really appreciate.
Solution
After many days trying many things, I finally could resolve this bug. Appereantly, It was happening with only Samsung Devices. I have testes many devices and emulator, but only in Samsung Devices this bug happens.
The solution was upgrade the kotlin version, that was 1.5.31 to 1.6.10, compose versions libs from 1.1.0 to 1.2.1, compileSdkVersion from 32 to 33 and compileComposeVersion from 1.0.5 to 1.1.1.
Answered By - Wilkison Martins
Answer Checked By - Gilberto Lyons (JavaFixing Admin)