Fatima Jamal
Fatima Jamal

Reputation: 159

How to solve FocusRequester is not initialized?

I'm working on three pages: StartPage, LoginPage, and SignupPage, and I'm integrating them into a HorizontalPager.

In the SignupPage, i'm using requestFocus on textField.

The navigation flow is Start -> Login, but I'm encountering this error when transitioning to the LoginPage, although focusRequester is defined on Signup. Based on my research this happened cause HorizontalPager, prepare next page although it's not visible on screen.

So how i can only triggers requestFocus only when SignupPage is visible, and disable it when it's not visible.

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() }

This is MyNavigation.kt

@Composable
fun MyAppNavigation(
    modifier: Modifier, authViewModel: AuthViewModel
) {
    val pages = registerInfoList.size
    val pagerState = rememberPagerState(pageCount = { pages })
    val scope = rememberCoroutineScope()

    Column(
        modifier = modifier.fillMaxSize()
    ) {
        HorizontalPager(
            state = pagerState, userScrollEnabled = false
        ) { page ->
            when (page) {
                0 -> StartPage(modifier = modifier,
                    toSignUp = {
                        scope.launch {
                            pagerState.animateScrollToPage(2)
                        }
                    }, toLogin = {
                        scope.launch {
                            pagerState.animateScrollToPage(1)
                        }
                    }, authViewModel = authViewModel
                )


                1 -> LoginPage(modifier = modifier,
                    toHome = {
                        scope.launch {
                            pagerState.animateScrollToPage(3)
                        }
                    }, toSignUp = {
                        scope.launch {
                            pagerState.animateScrollToPage(2)
                        }
                    },
                    toStart = {
                        scope.launch {
                            pagerState.animateScrollToPage(0)
                        }
                    }, authViewModel = authViewModel
                )


                2 -> SignupPage(
                    modifier = modifier,
                    toStart = {
                        scope.launch {
                            pagerState.animateScrollToPage(0)
                        }
                    },
                    toHome = {
                        scope.launch {
                            pagerState.animateScrollToPage(3)
                        }

                    },
                    authViewModel = authViewModel
                )

              ......
            }
        }
    }
}

LoginPage.kt

@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
@Composable
fun LoginPage(
    modifier: Modifier = Modifier,
    toSignUp: () -> Unit,
    toHome: () -> Unit,
    toStart: () -> Unit,
    authViewModel: AuthViewModel
) {
    ...................

    if (shouldShowDialog.value) {
        AlertDialog(shouldShowDialog = shouldShowDialog, message = errorMessage)
    }
    val authState = authViewModel.authState.observeAsState()

    LocalContext.current
    LaunchedEffect(authState.value) {
        when (authState.value) {
            is AuthState.Authenticated -> toHome()
            is AuthState.Error.InvalidCredentialsError -> {
                errorMessage = (authState.value as AuthState.Error.InvalidCredentialsError).message
                shouldShowDialog.value = true
            }

            is AuthState.Error.NetworkError -> {
                errorMessage = (authState.value as AuthState.Error.NetworkError).message
            }


            else -> Unit
        }
    }
    Scaffold(...................
    ) { innerPadding ->

        Column(
            .......
        ) {
            Text(
                ............
            )
            Spacer(modifier = Modifier.size(4.dp))
            TextField(............)

            Spacer(modifier = Modifier.size(32.dp))
            Text(.............)
            Spacer(modifier = Modifier.size(4.dp))
            TextField(.........)
            Text(
                ..........
            )

            Spacer(modifier = Modifier.height(32.dp))
            var colorButton by remember { mutableStateOf(Color.White) }
            Button(..........)
            Spacer(modifier = Modifier.height(8.dp))

            TextButton(.......)
        }
    }
}

SignupPage.kt

@Composable
fun SignupPage(
    modifier: Modifier = Modifier,
    toStart: () -> Unit,
    toHome: () -> Unit,
    authViewModel: AuthViewModel
) {
    .........


    // Added FocusRequesters for each TextField
    val emailFocusRequester = remember { FocusRequester() }
    val passwordFocusRequester = remember { FocusRequester() }
    val userNameFocusRequester = remember { FocusRequester() }
    val keyboardController = LocalSoftwareKeyboardController.current
    val context = LocalContext.current
    LaunchedEffect(authState.value) {
        when (authState.value) {
            is AuthState.Authenticated -> toHome()
            is AuthState.Error.NetworkError -> Toast.makeText(
                context,
                (authState.value as AuthState.Error.NetworkError).message, Toast.LENGTH_SHORT
            ).show()

            else -> Unit
        }
    }
    // Updated LaunchedEffect to handle focus and keyboard visibility

        LaunchedEffect(pagerState.currentPage) {//------> this where the crash happen
            when (pagerState.currentPage) {
                0 -> emailFocusRequester.requestFocus()
                1 -> passwordFocusRequester.requestFocus()
                2 -> keyboardController?.hide()
                3 -> keyboardController?.hide() // No TextField on this page
                4 -> userNameFocusRequester.requestFocus()
            }
            // Delay to ensure the focus change is processed
            delay(100)
            if (pagerState.currentPage in listOf(0, 1, 4)) {
                keyboardController?.show()
            } else {
                keyboardController?.hide()
            }
        }



    Scaffold(
        ..........
    ) { innerPadding ->


        Column(
            ..................
        ) {
            HorizontalPager(
                state = pagerState,
                userScrollEnabled = false
            ) { index ->
                Column(
                    ...............
                ) {
                    Text(
                        ..................
                    )
                    Spacer(modifier = Modifier.size(4.dp))
                    when (index) {
                        0 -> // email and name

                            TextField(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .focusRequester(emailFocusRequester),
                               )

                        1 -> {
                            TextField(modifier = Modifier
                                .fillMaxWidth()
                                .focusRequester(passwordFocusRequester))
                               

                        2 -> {

                            DatePicker { date ->
                                dateOfBirth = date.let {
                                    SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).format(it)
                                }
                            }
                        }

                        3 -> {
                            ExposedDropdownMenuBox(............)

                        4 -> // email and name

                            TextField(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .focusRequester(userNameFocusRequester),
                                )

                    Spacer(modifier = Modifier.size(4.dp))
                    Text(
                        ....................
                    )
                }
            }
            Spacer(modifier = Modifier.height(16.dp))
            Button(..............)
        }
    }
}

This is my current solution not sure if this meets best practices.

val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()

LaunchedEffect(key1 = lifecycleState, key2 = pagerState.currentPage) {
    when (lifecycleState) {
        Lifecycle.State.DESTROYED -> {
            keyboardController?.hide()
        }

        Lifecycle.State.INITIALIZED -> {
            keyboardController?.hide()
        }

        Lifecycle.State.CREATED -> {
            when (pagerState.currentPage) {
                0 -> emailFocusRequester.requestFocus()
                1 -> passwordFocusRequester.requestFocus()
                2 -> keyboardController?.hide()
                3 -> keyboardController?.hide() // No TextField on this page
                4 -> userNameFocusRequester.requestFocus()
            }
            // Delay to ensure the focus change is processed
            delay(100)
            if (pagerState.currentPage in listOf(0, 1, 4)) {
                keyboardController?.show()
            } else {
                keyboardController?.hide()
            }
        }

        Lifecycle.State.STARTED -> {
            when (pagerState.currentPage) {
                0 -> emailFocusRequester.requestFocus()
                1 -> passwordFocusRequester.requestFocus()
                2 -> keyboardController?.hide()
                3 -> keyboardController?.hide() // No TextField on this page
                4 -> userNameFocusRequester.requestFocus()
            }
            // Delay to ensure the focus change is processed
            delay(100)
            if (pagerState.currentPage in listOf(0, 1, 4)) {
                keyboardController?.show()
            } else {
                keyboardController?.hide()
            }
        }

        Lifecycle.State.RESUMED -> {
            when (pagerState.currentPage) {
                0 -> emailFocusRequester.requestFocus()
                1 -> passwordFocusRequester.requestFocus()
                2 -> keyboardController?.hide()
                3 -> keyboardController?.hide() // No TextField on this page
                4 -> userNameFocusRequester.requestFocus()
            }
            // Delay to ensure the focus change is processed
            delay(100)
            if (pagerState.currentPage in listOf(0, 1, 4)) {
                keyboardController?.show()
            } else {
                keyboardController?.hide()
            }
        }
    }
}

Upvotes: 1

Views: 183

Answers (1)

Myxoz
Myxoz

Reputation: 63

The Modifier.focusRequester(...) will only be called, if the Composable gets composed. Waiting is pretty imprecise. You could either wait longer or you could just code which looks like this:

val onFocusReady: ()->Unit={}
when(...){
   ...->onFocusReady={someFocusRequester.requestFocus()}
}
when(...){
   ...->TextField(modifier = Modifier
         .fillMaxWidth()
         .focusRequester(passwordFocusRequester))
        onFocusReady()
}

Which will insure the focus is initialized when you need it. Hope this helps.

Upvotes: 0

Related Questions