I have TvLazyRows inside TvLazyColumn. When I navigate to the end of all lists(position [20,20]) navigate to next screen and return back, focus is restored to the first visible position [15,1], not the position where I was before [20,20]. How can I restore focus to some specific position?

class MainActivity : ComponentActivity() {
private val rowItems = (0..20).toList()
private val rows = (0..20).toList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
MyAppNavHost(navController = navController)
}
}
@Composable
fun List(navController: NavController) {
val fr = remember {
FocusRequester()
}
TvLazyColumn( modifier = Modifier
.focusRequester(fr)
.fillMaxSize()
,
verticalArrangement = Arrangement.spacedBy(16.dp),
pivotOffsets = PivotOffsets(parentFraction = 0.05f),
) {
items(rows.size) { rowPos ->
Column() {
Text(text = "Row $rowPos")
TvLazyRow(
modifier = Modifier
.height(70.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
pivotOffsets = PivotOffsets(parentFraction = 0.0f),
) {
items(rowItems.size) { itemPos ->
var color by remember {
mutableStateOf(Color.Green)
}
Box(
Modifier
.width(100.dp)
.height(50.dp)
.onFocusChanged {
color = if (it.hasFocus) {
Color.Red
} else {
Color.Green
}
}
.background(color)
.clickable {
navController.navigate("details")
}
) {
Text(text = "Item ${itemPos.toString()}", Modifier.align(Alignment.Center))
}
}
}
}
}
}
LaunchedEffect(true) {
fr.requestFocus()
}
}
@Composable
fun MyAppNavHost(
navController: NavHostController = rememberNavController(),
startDestination: String = "list"
) {
NavHost(
navController = navController,
startDestination = startDestination
) {
composable("details") {
Details()
}
composable("list") { List(navController) }
}
}
@Composable
fun Details() {
Box(
Modifier
.background(Color.Blue)
.fillMaxSize()) {
Text("Second Screen", Modifier.align(Alignment.Center), fontSize = 48.sp)
}
}
}
versions
dependencies {
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'androidx.activity:activity-compose:1.7.1'
implementation platform('androidx.compose:compose-bom:2022.10.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
// Compose for TV dependencies
def tvCompose = '1.0.0-alpha06'
implementation "androidx.tv:tv-foundation:$tvCompose"
implementation "androidx.tv:tv-material:$tvCompose"
def nav_version = "2.5.3"
implementation "androidx.navigation:navigation-compose:$nav_version"
}
I tried passing FocusRequestor to each focusable element inside list. In that case I was able to restore focus. But For big amount of elements inside list it starts crashing with OutOfMemmoryError. So I need some another solution.
In Jetpack Compose, navigation is stateless by design, which means the focus state is not preserved by default. To achieve this, we have to maintain the state (the item's position) ourselves.
Below is a proposed solution that you can integrate into your code. Note that this solution works with the assumption that your list's items aren't dynamically changed. If they do, you may have to tweak the logic a bit:
private var lastFocusedItem by rememberSaveable{ mutableStateOf(Pair(0, 0)) }
lastFocusedItem..onFocusChanged { focusState ->
if (focusState.hasFocus) {
lastFocusedItem = Pair(rowPos, itemPos)
}
...
}
LaunchedEffect(true) {
// Request focus to the last focused item
focusRequesters[lastFocusedItem]?.requestFocus()
}
To achieve the last point, we need to have a map of FocusRequesters. We should use a map with keys as item positions (rowPos, itemPos) and values as FocusRequesters.
Here's the updated portion of your code that maintains and restores the focus of the last navigated item.
This is a two-step process:
FocusRequester as values. Use rememberSaveable to keep value during screen navigation.FocusRequester for each item and add it to the focusRequesters map.Your updated List composable might look something like this:
@Composable
fun List(navController: NavController) {
val focusRequesters = remember { mutableMapOf<Pair<Int, Int>, FocusRequester>() }
var lastFocusedItem by rememberSaveable{ mutableStateOf(Pair(0, 0)) }
TvLazyColumn(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp),
pivotOffsets = PivotOffsets(parentFraction = 0.05f),
) {
items(rows.size) { rowPos ->
Column() {
Text(text = "Row $rowPos")
TvLazyRow(
modifier = Modifier
.height(70.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
pivotOffsets = PivotOffsets(parentFraction = 0.0f),
) {
items(rowItems.size) { itemPos ->
var color by remember { mutableStateOf(Color.Green) }
val fr = remember { FocusRequester() }
focusRequesters[Pair(rowPos, itemPos)] = fr
Box(
Modifier
.width(100.dp)
.height(50.dp)
.focusRequester(fr)
.onFocusChanged {
color = if (it.hasFocus) {
lastFocusedItem = Pair(rowPos, itemPos)
Color.Red
} else {
Color.Green
}
}
.background(color)
.clickable {
navController.navigate("details")
}
) {
Text(text = "Item ${itemPos.toString()}", Modifier.align(Alignment.Center))
}
}
}
}
}
}
LaunchedEffect(true) {
focusRequesters[lastFocusedItem]?.requestFocus()
}
}
PS. to have composable methods is a bad idea. Composables should be pure functions without side effects.
@corvinav has posted a great approach. However, that approach has a possibility of crashing the app if the focusRequester is not attached to the element before requesting focus on it. Check out more details here: gBug - 276738340
Summarizing the problems in @corvinav's approach:
LaunchedEffect doesn't guarantee that the item will be ready to request focus.A better way would be to make use of navController.popBackStack() call on the details page which will restore the previous page and have many of the things preserved for you like scroll state across TvLazyRows and TvLazyColumns. One thing that it currently doesn't do is restore the focus the last focused item. You can do that by making use of focus requester, but with a slight modification to @corvinav's approach. We can make use of onGloballyPositioned modifier to guarantee that the item is in view and ready to accept focus.
Two key points to restore the previous state:
navController.pop* methods to restore previous pages. Checkout jetpack/compose/navigation for more detailsBackHandler as wellDemo:

Find the relevant code snippet for the above demo:
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun App() {
val navController = rememberNavController()
val lastFocusedItemId = remember { mutableStateOf<String?>(null) }
// To avoid requesting focus on the item more than once
val itemAlreadyFocused = remember { mutableStateOf(false) }
NavHost(navController = navController, startDestination = "home") {
composable("home") {
LaunchedEffect(Unit) {
// reset the value when home is launched
itemAlreadyFocused.value = false
}
HomePage(
navController = navController,
lastFocusedItemId = lastFocusedItemId,
itemAlreadyFocused = itemAlreadyFocused,
)
}
composable("movie") {
BackHandler {
navController.popBackStack()
}
Button(onClick = { navController.popBackStack() }) {
Text("Home")
}
}
}
}
@Composable
fun HomePage(
navController: NavController,
lastFocusedItemId: MutableState<String?>,
itemAlreadyFocused: MutableState<Boolean>,
) {
TvLazyColumn(Modifier.fillMaxSize()) {
// `rows` could be coming from your view model
itemsIndexed(rows) { index, rowData ->
MyRow(
rowData = rowData,
navController = navController,
lastFocusedItemId = lastFocusedItemId,
itemAlreadyFocused = itemAlreadyFocused,
)
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun MyRow(
rowData: List<MyItem>,
navController: NavController,
lastFocusedItemId: MutableState<String?>,
itemAlreadyFocused: MutableState<Boolean>,
) {
TvLazyRow(
horizontalArrangement = Arrangement.spacedBy(20.dp),
modifier = Modifier.focusRestorer()
) {
items(myItems) { item ->
key(item.id) {
val focusRequester = remember { FocusRequester() }
Card(
modifier = Modifier
.focusRequester(focusRequester)
.onGloballyPositioned {
if (
lastFocusedItemId.value == item.id &&
!itemAlreadyFocused.value
) {
focusRequester.requestFocus()
}
}
.onFocusChanged {
if (it.isFocused) {
lastFocusedItemId.value = item.id
itemAlreadyFocused.value = true
}
},
onClick = { navController.navigate("movie") }
)
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With