Assigned
Status Update
Comments
so...@google.com <so...@google.com> #2
Ralston, can you please help with the triage of this one?
ch...@gmail.com <ch...@gmail.com> #3
Just adding, I was seeing the same thing with Compose 1.7.6 but can't recreate in 1.8.0-beta02.
ch...@gmail.com <ch...@gmail.com> #4
Another piece of information. While my comment above is correct about it not crashing, it doesn't actually restore focus for loaded items.
ra...@google.com <ra...@google.com> #5
In the LazyList, instead of
LazyList{
if (pagingData.loadState.refresh is LoadState.Loading) {
items(count){
placeholderItem()
}
} else {
items(count){
actualItem()
}
}
}
Do you see this problem if you reuse the lazyItem?
LazyList{
items(count) {
if (pagingData.loadState.refresh is LoadState.Loading) {
placeholderItem()
} else {
actualItem()
}
}
}
If you use the same item in this way, you won't have to move focus from the placeholder to the actual item.
Description
Jetpack Compose component used: compose paging, focusRestorer, LazyColumn
Android Studio Build: Android Studio Ladybug Feature Drop | 2024.2.2
Kotlin version: 2.0.0
Steps to Reproduce or Code Sample to Reproduce:
1. Create LazyColumn with focusRestorer set, with default focusRequester
2. Populate a LazyColumn with data from LazyPagingItems
3. Set placeholders for initial load.
4. Open app and wait until the data loads
Stack trace (if applicable):
19:28:46.648 28386-28386 AndroidRuntime FATAL EXCEPTION: main (Ask Gemini)
Process: pl.test.testlazycolumn, PID: 28386
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() }
at androidx.compose.ui.focus.FocusRequester.findFocusTargetNode$ui_release(FocusRequester.kt:275)
at androidx.compose.ui.focus.FocusRequester.requestFocus-3ESFkO8(FocusRequester.kt:82)
at androidx.compose.ui.focus.FocusRequester.requestFocus-3ESFkO8$default(FocusRequester.kt:82)
at androidx.compose.ui.focus.FocusRestorerNode$onEnter$1.invoke(FocusRestorer.kt:120)
at androidx.compose.ui.focus.FocusRestorerNode$onEnter$1.invoke(FocusRestorer.kt:114)
at androidx.compose.ui.focus.FocusTransactionsKt.performCustomEnter-Mxy_nc0(FocusTransactions.kt:940)
at androidx.compose.ui.focus.FocusTransactionsKt.performCustomRequestFocus-Mxy_nc0(FocusTransactions.kt:412)
at androidx.compose.ui.focus.FocusTargetNode.requestFocus-3ESFkO8(FocusTargetNode.kt:110)
at androidx.compose.ui.platform.AndroidComposeView$requestFocus$focusSearchResult$1.invoke(AndroidComposeView.android.kt:1075)
at androidx.compose.ui.platform.AndroidComposeView$requestFocus$focusSearchResult$1.invoke(AndroidComposeView.android.kt:1070)
at androidx.compose.ui.focus.FocusOwnerImpl$focusSearch$1.invoke(FocusOwnerImpl.kt:324)
at androidx.compose.ui.focus.FocusOwnerImpl$focusSearch$1.invoke(FocusOwnerImpl.kt:320)
at androidx.compose.ui.focus.TwoDimensionalFocusSearchKt.findChildCorrespondingToFocusEnter--OM-vw8(TwoDimensionalFocusSearch.kt:157)
at androidx.compose.ui.focus.TwoDimensionalFocusSearchKt.twoDimensionalFocusSearch-sMXa3k8(TwoDimensionalFocusSearch.kt:63)
at androidx.compose.ui.focus.FocusTraversalKt.focusSearch-0X8WOeE(FocusTraversal.kt:134)
at androidx.compose.ui.focus.FocusOwnerImpl.focusSearch-ULY8qGw(FocusOwnerImpl.kt:320)
at androidx.compose.ui.platform.AndroidComposeView.requestFocus(AndroidComposeView.android.kt:1070)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3377)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3326)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3377)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3326)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3377)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3326)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3377)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3331)
at android.view.View.requestFocus(View.java:14450)
at android.view.View.requestFocus(View.java:14392)
at android.view.View.rootViewRequestFocus(View.java:8187)
at android.view.View.clearFocusInternal(View.java:8173)
at android.view.View.clearFocus(View.java:8149)
at android.view.ViewGroup.clearFocus(ViewGroup.java:1191)
at androidx.compose.ui.platform.AndroidComposeView.onClearFocusForOwner(AndroidComposeView.android.kt:1135)
at androidx.compose.ui.platform.AndroidComposeView.access$onClearFocusForOwner(AndroidComposeView.android.kt:234)
at androidx.compose.ui.platform.AndroidComposeView$focusOwner$4.invoke(AndroidComposeView.android.kt:288)
at androidx.compose.ui.platform.AndroidComposeView$focusOwner$4.invoke(AndroidComposeView.android.kt:288)
at androidx.compose.ui.focus.FocusOwnerImpl.invalidateOwnerFocusState(FocusOwnerImpl.kt:430)
at androidx.compose.ui.focus.FocusOwnerImpl.access$invalidateOwnerFocusState(FocusOwnerImpl.kt:67)
at androidx.compose.ui.focus.FocusOwnerImpl$focusInvalidationManager$1.invoke(FocusOwnerImpl.kt:83)
at androidx.compose.ui.focus.FocusOwnerImpl$focusInvalidationManager$1.invoke(FocusOwnerImpl.kt:83)
19:28:46.649 28386-28386 AndroidRuntime at androidx.compose.ui.focus.FocusInvalidationManager.invalidateNodesOptimized(FocusInvalidationManager.kt:165) (Ask Gemini)
at androidx.compose.ui.focus.FocusInvalidationManager.invalidateNodes(FocusInvalidationManager.kt:114)
at androidx.compose.ui.focus.FocusInvalidationManager.access$invalidateNodes(FocusInvalidationManager.kt:36)
at androidx.compose.ui.focus.FocusInvalidationManager$setUpOnRequestApplyChangesListener$1.invoke(FocusInvalidationManager.kt:93)
at androidx.compose.ui.focus.FocusInvalidationManager$setUpOnRequestApplyChangesListener$1.invoke(FocusInvalidationManager.kt:93)
at androidx.compose.ui.platform.AndroidComposeView.onEndApplyChanges(AndroidComposeView.android.kt:1282)
at androidx.compose.ui.node.UiApplier.onEndChanges(UiApplier.android.kt:46)
at androidx.compose.runtime.CompositionImpl.applyChangesInLocked(Composition.kt:1038)
at androidx.compose.runtime.CompositionImpl.applyChanges(Composition.kt:1067)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:684)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:591)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:39)
at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:108)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:69)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1337)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1348)
at android.view.Choreographer.doCallbacks(Choreographer.java:952)
at android.view.Choreographer.doFrame(Choreographer.java:878)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1322)
at android.os.Handler.handleCallback(Handler.java:958)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8177)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.runtime.PausableMonotonicFrameClock@af51806, androidx.compose.ui.platform.MotionDurationScaleImpl@2513ec7, StandaloneCoroutine{Cancelling}@2a28cf4, AndroidUiDispatcher@ee08c1d]
So in my Android TV app I want to display placeholders during initial load and I want user to be able to scroll through the items while they're loading. After the data is reloaded I want to focus item at the same position as the lastly focused placeholder, but even if I want to focus the first one - it doesn't work.
If I set the `focusRestorer { focusRequester }`, right after the loading is done, the app crashes with exception above. If I don't set the default requester for focusRestorer - I use just the `focusRestorer()` then the app doesn't crash, but obviously I won't get the behaviour that I need.
Here's the code:
```
@Composable
fun BoxesColumn(modifier: Modifier = Modifier) {
val scope = rememberCoroutineScope()
val flow = remember {
val pager = Pager(PagingConfig(pageSize = 50)) {
object : PagingSource<Int, String>() {
override fun getRefreshKey(state: PagingState<Int, String>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
delay(5000)
return LoadResult.Page(
data = (1..50).map { it.toString() },
prevKey = null,
nextKey = null,
)
}
}
}
pager.flow.cachedIn(scope)
}
val pagingData = flow.collectAsLazyPagingItems()
val fr = remember { FocusRequester() }
var lastFocusedPlaceholder by remember { mutableIntStateOf(0) }
var positioned by remember { mutableStateOf(false) }
LazyColumn(
modifier = modifier
.fillMaxSize()
.focusRestorer(fr),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
if (pagingData.loadState.refresh is LoadState.Loading) {
items(5) { index ->
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
Box(
modifier = Modifier
.let {
if (index == 0) {
it.focusRequester(fr)
} else {
it
}
}
.onFocusChanged {
if (pagingData.loadState.refresh is LoadState.Loading) {
lastFocusedPlaceholder = index
}
println("focused = $index")
}
.focusable(interactionSource = interactionSource)
.background(if (isFocused) Color.Blue else Color.Gray)
.size(100.dp),
contentAlignment = Alignment.Center,
) {
Text(text = "P$index")
}
}
} else {
items(pagingData.itemCount, key = { pagingData[it].orEmpty() }) { index ->
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
Box(
modifier = Modifier
.let {
if (index == lastFocusedPlaceholder) {
it
.focusRequester(fr)
.onGloballyPositioned {
if (!positioned) {
fr.requestFocus()
positioned = true
}
}
} else {
it
}
}
.focusable(interactionSource = interactionSource)
.background(if (isFocused) Color.Red else Color.Green)
.size(100.dp),
contentAlignment = Alignment.Center,
) {
Text(text = pagingData[index].orEmpty())
}
}
}
}
}
```