Hi, I am encountering an issue with keyboard behav...
# compose-android
v
Hi, I am encountering an issue with keyboard behavior in Jetpack Compose.
Here's the scenario: 1. When using a fully Compose-based UI, switching between TextField components works as expected. The keyboard remains open while navigating between text fields, which is the desired behavior. 2. However, when I use a hybrid approach (combining XML and Compose), the keyboard behaves differently. Specifically, when switching between TextField components, the keyboard closes and reopens, which is undesirable. I suspect this might be related to how focus is handled between the XML and Compose layers. Here are the key questions I have: 1. Why does this behavior occur in the hybrid approach but not in the fully Compose-based approach? 2. How can I ensure the keyboard remains open when switching between text fields in the hybrid XML + Compose setup? Any insights, code snippets, or best practices to address this issue would be greatly appreciated!
Copy code
class MainActivity : ComponentActivity() {

    private lateinit var binding: MainActivityBinding

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
//                binding = MainActivityBinding.inflate(layoutInflater)
//                setContentView(binding.root)
//                binding.firstComposeView.setContent {
//                    BasicTextFieldExample(PaddingValues(20.dp))
//                }
//                binding.secondComposeView.setContent {
//                    BasicTextFieldExample(PaddingValues(20.dp))
//                }
        setContent {
            BasicTextFieldExampleTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Column(
                        Modifier.fillMaxSize()
                    ) {
                        BasicTextFieldExample(PaddingValues(20.dp))
                        Spacer(Modifier.height(100.dp))
                        BasicTextFieldExample(innerPadding)
                    }
                }
            }
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun BasicTextFieldExample(innerPadding: PaddingValues) {
    val state = rememberTextFieldState()

    val interactionSource = remember { MutableInteractionSource() }
    val basicTextFieldViewState by remember {
        mutableStateOf(
            BasicTextFieldViewState(
                inputTransformation = InputTransformation.maxLength(12)
                    .then(DigitsOnlyTransformation),
                outputTransformation = GroupingOutputTransformation(4, "-"),
                lineLimits = TextFieldLineLimits.SingleLine
            )
        )
    }

    BasicTextFieldView(
        modifier = Modifier
            .padding(innerPadding)
            .wrapContentSize(),
        state = state,
        interactionSource = interactionSource,
        basicTextFieldViewState = basicTextFieldViewState,
        prefix = {
            Text(
                "Abc-"
            )
        }
    )
}

data class BasicTextFieldViewState(
    val inputTransformation: InputTransformation? = null,
    val outputTransformation: OutputTransformation? = null,
    val isEnabled: Boolean = true,
    val isError: Boolean = false,
    val lineLimits: TextFieldLineLimits,
)

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BasicTextFieldView(
    modifier: Modifier,
    state: TextFieldState,
    interactionSource: MutableInteractionSource,
    basicTextFieldViewState: BasicTextFieldViewState,
    prefix: @Composable () -> Unit,
) {
    Column(modifier) {
        BasicTextField(
            state = state,
            modifier = Modifier
                .fillMaxWidth()
                .padding(20.dp),
            interactionSource = interactionSource,
            inputTransformation = basicTextFieldViewState.inputTransformation,
            outputTransformation = basicTextFieldViewState.outputTransformation,
            enabled = basicTextFieldViewState.isEnabled,
            lineLimits = TextFieldLineLimits.SingleLine,
            textStyle = LocalTextStyle.current,
            decorator = TextFieldDefaults.decorator(
                state = state,
                outputTransformation = basicTextFieldViewState.outputTransformation,
                lineLimits = basicTextFieldViewState.lineLimits,
                enabled = basicTextFieldViewState.isEnabled,
                label = {
                    Text(
                        "Label"
                    )
                },
                isError = basicTextFieldViewState.isError,
                interactionSource = interactionSource,
                prefix = prefix,
                placeholder = {
                    Text(
                        "Number"
                    )
                },
            )
        )
    }
}

object DigitsOnlyTransformation : InputTransformation {
    override val keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)

    override fun TextFieldBuffer.transformInput() {
        if (!asCharSequence().isDigitsOnly()) {
            revertAllChanges()
        }
    }
}

@Stable
data class GroupingOutputTransformation(
    private val groupSize: Int,
    private val groupDelimiter: String,
) : OutputTransformation {
    override fun TextFieldBuffer.transformOutput() {
        repeat((length - 1) / groupSize) {
            insert(it + (it + 1) * groupSize, groupDelimiter)
        }
    }
}
Expected Output :- YouTube Actual Output :- Youtube
z
I think this was fixed in September, not sure what release it's (going to be) in
k
thank you so much. I'll take a look
Hi, I am using latest version of compose with xml and i'm still seeing keybaord switch from one textfield to another.
Copy code
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.OutputTransformation
import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldDecorator
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.insert
import androidx.compose.foundation.text.input.maxLength
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.then
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.core.text.isDigitsOnly
import com.example.baisctextfieldexample.databinding.MainActivityBinding
import com.example.baisctextfieldexample.ui.theme.BaiscTextFieldExampleTheme
import kotlinx.coroutines.flow.collectLatest

class MainActivity : ComponentActivity() {

    private lateinit var binding: MainActivityBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = MainActivityBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.firstComposeView.setContent {
            BasicTextFieldExample(PaddingValues(20.dp))
        }
        binding.secondComposeView.setContent {
            BasicTextFieldExample(PaddingValues(20.dp))
        }

//        enableEdgeToEdge()
//        setContent {
//            BaiscTextFieldExampleTheme {
//                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
//                    Column(
//                        Modifier.fillMaxSize()
//                    ) {
//                        BasicTextFieldExample(PaddingValues(20.dp))
//                        Spacer(Modifier.height(100.dp))
//                        BasicTextFieldExample(innerPadding)
//                    }
//                }
//            }
//        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BasicTextFieldExample(innerPadding: PaddingValues) {
    val state = rememberTextFieldState(initialText = "ABC-")
    val interactionSource = remember { MutableInteractionSource() }
    val basicTextFieldViewState by remember {
        mutableStateOf(
            BasicTextFieldViewState(
                inputTransformation = InputTransformation.maxLength(12)
                    .then(DigitsOnlyTransformation),
//                outputTransformation = GroupingOutputTransformation(4, "-"),
                lineLimits = TextFieldLineLimits.SingleLine
            )
        )
    }

    LaunchedEffect(Unit) {
        snapshotFlow {
            state.text
        }.collectLatest {
            Log.e("snapshotFlow", "$it")
        }
    }

    BasicTextFieldView(
        modifier = Modifier
            .padding(innerPadding)
            .wrapContentSize(),
        state = state,
        interactionSource = interactionSource,
        basicTextFieldViewState = basicTextFieldViewState,
    )
}

data class BasicTextFieldViewState(
    val inputTransformation: InputTransformation? = null,
    val outputTransformation: OutputTransformation? = null,
    val isEnabled: Boolean = true,
    val isError: Boolean = false,
    val lineLimits: TextFieldLineLimits,
)

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BasicTextFieldView(
    modifier: Modifier,
    state: TextFieldState,
    interactionSource: MutableInteractionSource,
    basicTextFieldViewState: BasicTextFieldViewState,
) {
    Column(modifier) {
        BasicTextField(
            state = state,
            modifier = Modifier
                .fillMaxWidth()
                .padding(20.dp),
            interactionSource = interactionSource,
            inputTransformation = basicTextFieldViewState.inputTransformation,
            outputTransformation = basicTextFieldViewState.outputTransformation,
            enabled = basicTextFieldViewState.isEnabled,
            lineLimits = TextFieldLineLimits.SingleLine,
            textStyle = LocalTextStyle.current,
            decorator = TextFieldDefaults.decorator(
                state =state,
                enabled = true,
                lineLimits = TextFieldLineLimits.Default,
                interactionSource = interactionSource,
                outputTransformation = basicTextFieldViewState.outputTransformation,
                label = {
                        Text(
                            "Label"
                        )
                    },
            ),
//            decorator = {
//                TextFieldDefaults.dec(
//                    value = state.text.toString(),
//                    innerTextField = it,
//                    enabled = basicTextFieldViewState.isEnabled,
//                    label = {
//                        Text(
//                            "Label"
//                        )
//                    },
//                    isError = basicTextFieldViewState.isError,
//                    interactionSource = interactionSource,
//                    singleLine = true,
//                    visualTransformation = VisualTransformation.None
//                )
//            }
        )
    }
}

object DigitsOnlyTransformation : InputTransformation {
    override val keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)

    override fun TextFieldBuffer.transformInput() {
        if (!asCharSequence().isDigitsOnly()) {
            revertAllChanges()
        }
    }
}

//@Stable
//data class GroupingOutputTransformation(
//    private val groupSize: Int,
//    private val groupDelimiter: String,
//) : OutputTransformation {
//    override fun TextFieldBuffer.transformOutput() {
//        repeat((length - 1) / groupSize) {
//            insert(it + (it + 1) * groupSize, groupDelimiter)
//        }
//    }
//}
Copy code
[versions]
agp = "8.7.3"
kotlin = "2.1.0"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.0"
composeBom = "2025.01.00"
materialVersion3 = "1.4.0-alpha06"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "materialVersion3" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }