androidkotlinandroid-jetpack-composeandroid-tvandroid-jetpack-compose-tv

Jetpack Compose - Is it possible to use focusRestorer in a LazyRow with changing content?


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 Cards 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?


Solution

  • 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 Cards 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.