In the JetStream tv app sample, it has a createInitialFocusRestorerModifiers()
function which acts as the focus restorers of a TvLazyRow/Column for its children.
As said from the function's KDoc:
Returns a set of modifiers [FocusRequesterModifiers] which can be used for restoring focus and specifying the initially focused item.
Its usage:
LazyRow(modifier.then(modifiers.parentModifier) {
item1(modifier.then(modifiers.childModifier) {...}
item2 {...}
item3 {...}
...
}
The proper behavior should be the following image. Pressing DPad down should focus to the first item, and up should bring to the last saved state focus child.
Though, the problem here is it does not restore focus on navigation [popBackStack()
] so I tried to integrate the said function with this answer.
Current code:
// var anItemHasBeenFocused: Boolean = false
// var lastFocusedItem: Pair<Int, Int> = ...
TvLazyRow(
modifier = focusRestorerModifiers.parentModifier,
pivotOffsets = PivotOffsets(parentFraction = 0F)
) {
itemsIndexed(items = items) { columnIndex, item ->
val focusRequester = remember { FocusRequester() }
RowItem(
modifier = Modifier
.focusRequester(focusRequester)
.ifElse(
condition = columnIndex == 0,
ifTrueModifier = focusRestorerModifiers.childModifier
)
.onPlaced {
val shouldFocusThisItem = lastFocusedItem.first == rowIndex
&& lastFocusedItem.second == columnIndex
&& !anItemHasBeenFocused
if (shouldFocusThisItem) {
// The stacktrace points here.
focusRequester.requestFocus()
}
}
.onFocusChange {
if(it.isFocused) {
onFocusedChange(item, columnIndex)
}
}
.focusProperties {
if (rowIndex == 0) {
up = FocusRequester.Cancel
} else if (isLastIndex) {
down = FocusRequester.Cancel
}
},
item = item,
isLastIndex = columnIndex == items.lastIndex,
onClick = onNavigateToSecondScreen
)
}
The code worked fine on the first item only, I was able to go back without any errors from the second screen after navigating thru the first item.
When I tried to scroll to the end item of the list - making the first item hidden from the current view - and tried to go to my second screen and immediately pop back from it, the app constantly throws:
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() }
Question: How do I achieve this kind of focus behavior? Being able to combine both initial state focus restorers and navigation state focus restoring?
1. Update to Latest builds/versions to avoid dealing with issues which might have already been fixed
Since the build id in your repository is severely out of date, I would first recommend that you
1.0.0-alpha08
as of today)settings.gradle.kts
. Checkout latest working androidx snapshot builds for finding the latest build ids.2. Coming to your question (WORKAROUND)
Instead of passing down lastFocusedItem state to all the child components, we can make use of CompositionLocal to store the stuff. Following are some sample utility composition locals that we will make use of, in the demo below.
With the above utilities in place, we can now create a modifier which would focus the item when the Nav Destination is launched. In this modifier, we will check if this was the last focused item before the Nav destination was changed, and if yes, we will request focus onto this item.
Usage of this modifier is pretty straight forward. Wrap the screens with the utility providers we created above and assign the
focusOnMount
modifier to all the focusable items (like buttons, cards, tabs, etc.) on your page.You might also observe that when the app first launches, nothing is focused. To fix this, you can update the
LocalLastFocusedItemPerDestinationProvider
to initialise with the keys of items that should get focus initially.