Assigned
Status Update
Comments
so...@google.com <so...@google.com> #2
This actually has nothing to do with NavHostFragment, but is the behavior of NavController's setGraph().
When you call navController.setGraph(R.navigation.navigation_graph), it stores that ID and will restore that ID automatically.
If you were to instead use:
NavInflater navInflater = new NavInflater(this, navController.getNavigatorProvider());
navController.setGraph(navInflater.inflate(R.navigation.navigation_graph));
Then NavController would not restore the graph itself and the call to restoreState() you point out would only restore the back stack state, etc. but would wait for you to call setGraph again.
You're right that the inconsistency between the two setGraph methods is concerning. We'll take a look.
When you call navController.setGraph(R.navigation.navigation_graph), it stores that ID and will restore that ID automatically.
If you were to instead use:
NavInflater navInflater = new NavInflater(this, navController.getNavigatorProvider());
navController.setGraph(navInflater.inflate(R.navigation.navigation_graph));
Then NavController would not restore the graph itself and the call to restoreState() you point out would only restore the back stack state, etc. but would wait for you to call setGraph again.
You're right that the inconsistency between the two setGraph methods is concerning. We'll take a look.
ch...@gmail.com <ch...@gmail.com> #3
Turns out, we already had a tracking bug for this issue, will follow up on that other one.
ch...@gmail.com <ch...@gmail.com> #4
Thank you for promptly replying to my report. You are right that the issue you've just mentioned is similar to mine. I shall continue observing the progress over there.
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())
}
}
}
}
}
```