Reputation: 159
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
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