Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions app/src/main/java/app/gamenative/ui/component/QuickMenu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
Expand Down Expand Up @@ -49,6 +50,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import kotlinx.coroutines.delay
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand Down Expand Up @@ -151,26 +153,26 @@ fun QuickMenu(
AnimatedVisibility(
visible = isVisible,
enter = slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
initialOffsetX = { fullWidth -> -fullWidth },
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMediumLow
)
),
exit = slideOutHorizontally(
targetOffsetX = { fullWidth -> fullWidth },
targetOffsetX = { fullWidth -> -fullWidth },
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium
)
),
modifier = Modifier.align(Alignment.CenterEnd)
modifier = Modifier.align(Alignment.CenterStart)
) {
Surface(
modifier = Modifier
.width(adaptivePanelWidth(280.dp))
.fillMaxHeight(),
shape = RoundedCornerShape(topStart = 24.dp, bottomStart = 24.dp),
shape = RoundedCornerShape(topEnd = 24.dp, bottomEnd = 24.dp),
color = MaterialTheme.colorScheme.surface,
tonalElevation = 2.dp,
shadowElevation = 24.dp,
Expand Down Expand Up @@ -234,12 +236,16 @@ fun QuickMenu(
}
}

LaunchedEffect(isVisible) {
LaunchedEffect(isVisible, menuItems.size) {
if (isVisible) {
try {
firstItemFocusRequester.requestFocus()
} catch (_: Exception) {
// Focus request may fail if composition is not ready
repeat(3) {
try {
firstItemFocusRequester.requestFocus()
return@LaunchedEffect
} catch (_: Exception) {
// Focus request may fail if composition is not ready.
delay(80)
}
Comment on lines +239 to +248
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

FocusRequester.requestFocus() Jetpack Compose return value boolean failure

💡 Result:

FocusRequester.requestFocus() does return a Boolean (in newer Compose it’s fun requestFocus(...): Boolean). It returns true if focus was successfully requested, or false if the focus request was canceled. [1]

What “failure” usually means in practice:

  • The request was canceled by focus rules (for example, focusProperties { enter/exit = Cancel }, or the target can’t be focused / isn’t focusable). In that case you’ll typically just get false. [1][2]
  • The FocusRequester isn’t attached to any node yet (you forgot Modifier.focusRequester(...), the modifier order is wrong, or you’re calling it before the node exists—common with subcomposition like Scaffold). That often throws IllegalStateException: FocusRequester is not initialized… rather than returning false. [3]
  • You called it at the wrong time (during composition / before layout). Google’s guidance is to request focus in response to an event or from a side-effect (LaunchedEffect), and ensure the requester is associated first. [2][4]

How to debug/handle it:

  • Check the boolean:
    val ok = focusRequester.requestFocus()
  • Also observe what actually happened (since false doesn’t tell you why):
    Modifier.onFocusChanged { state -> /* state.isFocused */ }
  • Ensure the basics:
    • val focusRequester = remember { FocusRequester() } [3]
    • target has Modifier.focusRequester(focusRequester) and is actually focusable (e.g., TextField, or add focusTarget()/focusable() where appropriate) [2]
    • call requestFocus() from LaunchedEffect/event after the node is in the hierarchy (and inside Scaffold content if Scaffold is involved). [3]

Sources: [1] FocusRequester API (requestFocus returns Boolean, false if canceled) (developer.android.com), [2] “Change focus behavior” (how to request focus; focus properties can prevent focus) (developer.android.com), [3] IllegalStateException: FocusRequester is not initialized… explanation and common fixes (stackoverflow.com), [4] Compose focus codelab warning that calling requestFocus() without being associated can crash (developer.android.com).

Citations:


🏁 Script executed:

# Search for the QuickMenu.kt file and examine the code around lines 239-248
find . -name "QuickMenu.kt" -type f

Repository: utkarshdalal/GameNative

Length of output: 127


🏁 Script executed:

cat -n app/src/main/java/app/gamenative/ui/component/QuickMenu.kt | sed -n '235,255p'

Repository: utkarshdalal/GameNative

Length of output: 703


Check the Boolean return value of requestFocus() to properly retry focus requests.

At line 243–244, requestFocus() returns a Boolean but the code ignores it and always returns immediately. This skips retries when focus is denied (returns false) without throwing an exception. Only exception-triggered retries work, leaving cancellations unhandled.

Proposed fix
 LaunchedEffect(isVisible, menuItems.size) {
     if (isVisible) {
         repeat(3) {
             try {
-                firstItemFocusRequester.requestFocus()
-                return@LaunchedEffect
+                val focused = firstItemFocusRequester.requestFocus()
+                if (focused) {
+                    return@LaunchedEffect
+                }
             } catch (_: Exception) {
                 // Focus request may fail if composition is not ready.
-                delay(80)
             }
+            delay(80)
         }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LaunchedEffect(isVisible, menuItems.size) {
if (isVisible) {
try {
firstItemFocusRequester.requestFocus()
} catch (_: Exception) {
// Focus request may fail if composition is not ready
repeat(3) {
try {
firstItemFocusRequester.requestFocus()
return@LaunchedEffect
} catch (_: Exception) {
// Focus request may fail if composition is not ready.
delay(80)
}
LaunchedEffect(isVisible, menuItems.size) {
if (isVisible) {
repeat(3) {
try {
val focused = firstItemFocusRequester.requestFocus()
if (focused) {
return@LaunchedEffect
}
} catch (_: Exception) {
// Focus request may fail if composition is not ready.
}
delay(80)
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/ui/component/QuickMenu.kt` around lines 239
- 248, The LaunchedEffect in QuickMenu.kt currently ignores the Boolean result
of firstItemFocusRequester.requestFocus() and always returns, so retries only
happen on exceptions; change the logic in the LaunchedEffect (inside
QuickMenu/LaunchedEffect) to capture the Boolean from
firstItemFocusRequester.requestFocus(), only exit the LaunchedEffect when it
returns true, and otherwise delay and retry (up to the existing repeat count),
while preserving the existing try/catch for exceptions that also trigger a
delay.

}
}
}
Expand Down Expand Up @@ -323,9 +329,7 @@ private fun QuickMenuItemRow(

Text(
text = stringResource(item.labelResId),
style = MaterialTheme.typography.bodyLarge.copy(
fontWeight = if (isFocused && isEnabled) FontWeight.SemiBold else FontWeight.Normal
),
style = MaterialTheme.typography.bodyLarge,
color = when {
!isEnabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = disabledAlpha)
isFocused -> accentColor
Expand Down
Loading