I am working on a TV app with many LazyRows
of content. Most are static, but some can be changed depending on the user's actions. For example, a Keep Watching row with Card
s that lead to content the user is currently watching.
Ideally we want to use focusRestorer
so the row can default to focusing on the first item, then remember what to focus on if the user leaves the row and returns. Unfortunately since the user can click on a Card
, navigate away, finish the content, then return to the page, the default that focusRestorer
takes may not exist when the page recomposes. This causes a crash.
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() }
I've been able to prevent crashes and refocus when returning to the page by taking out focusRestorer
and instead remembering the last focused item's index and content ID. I then use those to requestFocus
when positioned, sort of like this:
KeepWatchingCard(content = content, cardModifier = Modifier
.focusRequester(requester)
.onGloballyPositioned {
if (shouldRequestFocus) { // checks if this was the last focused row and item
requester.requestFocus()
onFocus() // sets a variable elsewhere so we don't requestFocus multiple times
}
}, onClick = {
lastContentId.intValue = it.id
lastIndex.intValue = index
onClick(it) // navigates to content
})
It's not perfect though since I do not have functionality from focusRestorer
like a default or going back to my spot after scrolling further down the page. Giving each Card
its own FocusRequester
and saving that to use just causes crashes when an item is removed or not yet visible.
Any suggestions to make focusRestorer
work with a dynamic list or better duplicate its effects?
I was able to get something close to the behavior I'm looking for by using onGloballyPositioned
and focusProperties
on the row. Here is most of the relevant code, maybe it will help someone else. And of course feel free to let me know if this solution is lacking in some way.
LazyRow(
state = listState,
modifier = Modifier
.focusRequester(requester)
.onGloballyPositioned {
if (shouldRequestFocus()) { // checks if this was the last focused section and we haven't already requested focus
requester.requestFocus() // triggers onEnter
onFocus() // sets a variable elsewhere so we don't requestFocus multiple times
}
}
.focusProperties {
onEnter = {
// onEnter is bugged and gets called whenever you click one of the cards, so navigatingAway prevents this
if (!navigatingAway && !requester.restoreFocusedChild()) {
scope.launch {
listState.scrollToItem(0)
requester.requestFocus()
}
}
}
onExit = { requester.saveFocusedChild() }
}
) {
// all the content
}
And then for each of the cards inside:
KeepWatchingCard(
content = content,
onClick = {
navigatingAway = true // keeps onEnter from being called when we click
requester.saveFocusedChild()
onClick(it) // navigates to content
})
It would be much easier if focusRestorer
was more flexible but this does more or less the same thing now. Whenever the list of Card
s changes in some way, we just set the focus to the first item in the list. Any other time it restores focus to the appropriate Card
.