androidandroid-jetpack-compose

TextField with AnimatedVisibility should show keyboard (requestFocus)


I have a TextField that is displayed when the user clicks on the search button, and the visibility is animated. The problem is that when I request the focus to show the keyboard, the composition is not finished, so focusRequester was not initialized, causing the crash. One possible solution would be to add a delay with the same amount of time as the animation but this is not safe as it could have a racing condition and the crash happen in rare cases. Any other alternative?

@Composable
fun MyComponent() {
    var expanded by rememberSaveable { mutableStateOf(false) }
    val focusRequester = remember { FocusRequester() }

    AnimatedVisibility(
        visible = expanded,
        enter = expandHorizontally(animationSpec = tween(300)) + fadeIn(),
        exit = shrinkHorizontally(animationSpec = tween(300)) + fadeOut()
    ) {
        TextField(
            modifier = Modifier.focusRequester(focusRequester),
            ...)

    }

    IconButton(onClick = {
        expanded = !expanded
        focusRequester.requestFocus()
    }) {
        MyImage(
            icon = R.drawable.ic_search,
            modifier = Modifier.size(56.dp)
        )
    }

}
java.lang.IllegalStateException: 

FocusRequester is not initialized. Here are some possible fixes:
1. Remember the FocusRequester: val focusRequester = remember { FocusRequester() }
2. Did you forget to add a Modifier.focusRequester() ?
3. Are you attempting to request focus during composition? Focus requests should be made in response to some event. Eg Modifier.clickable { focusRequester.requestFocus() }

Solution

  • So instead of requesting the focus immediately after setting the expanded state to true, you should wait for the animation transition to complete first to ensure your TextField has been launched into composition, before requesting focus.

    A handy tool to do this with is the MutableTransitionState which you can use to know the current animation state of the AnimatedVisibility. In using this, you can put it in a LaunchedEffect and request the focus after the transition is completed.

    Here's your re-written example below:

    @Composable
    fun MyComponent() {
        val visibilityState = remember { MutableTransitionState(false) }
        val focusRequester = remember { FocusRequester() }
    
        AnimatedVisibility(
            visibleState = visibilityState,
            enter = expandHorizontally(animationSpec = tween(300)) + fadeIn(),
            exit = shrinkHorizontally(animationSpec = tween(300)) + fadeOut()
        ) {
            TextField(
                modifier = Modifier
                    .fillMaxWidth()
                    .focusRequester(focusRequester),
                value = "",
                onValueChange = {},
            )
        }
    
        IconButton(onClick = {
            visibilityState.targetState = !visibilityState.currentState
        }) {
            Icon(
                modifier = Modifier.size(56.dp),
                imageVector = Icons.Outlined.Search,
                contentDescription = "Search"
            )
        }
    
        LaunchedEffect(visibilityState.currentState) {
            if (visibilityState.isIdle && visibilityState.currentState) {
                focusRequester.requestFocus()
                println("Focus requested on TextField")
            }
        }
    }
    

    You can find more examples on using MutableTransitionState here