https://kotlinlang.org logo
#compose
Title
# compose
r

rsktash

08/27/2021, 5:40 PM
There is an issue with focus restore on recomposition. When we use dynamic list of TextFields and add new TextField on last text change the focus is passed to the new TextField https://github.com/rustamsmax/compose-report
@Ralston Da Silva
How the focus restoration works?
r

Ralston Da Silva

08/27/2021, 6:36 PM
Can you help me by simplifying the example a bit? (The FocusRequesters don't seem to be used) and also if you could, send a video of the problem you are seeing?
When I start editing new textField is being added but after recomposition the focus is set to the newly added textfield not the editing one
r

Ralston Da Silva

08/27/2021, 7:07 PM
I think your problem is not focus related, you are capturing values in the lambdas while iterating through the list, and then editing the list you are iterating on:
Copy code
@Composable
fun TestFocusWithDynamicContent() {
  val list = remember { mutableStateListOf("") }
  val focusRequesters = list.indices.map { remember(it) { FocusRequester() } }
  Column {
    list.forEachIndexed { index, item ->
      if (index != list.lastIndex) {
        OutlinedTextField(
          value = item,
          onValueChange = {
            list[index] = it
            if (index == list.lastIndex && it.isNotEmpty()) list.add("")
          },
          modifier = Modifier
            .fillMaxWidth()
            .focusRequester(focusRequesters[index])
            .relocate("$index"),
          placeholder = { Text(text = "type anything") }
        )
      }
      else {
        OutlinedTextField(
          value = "",
          onValueChange = {
            if (index == list.lastIndex && it.isNotEmpty()) list.add("")
          }
        )
      }
    }

  }
}
Moving the index check to the onValue changed solves this issue:
Copy code
@Composable
fun TestFocusWithDynamicContent() {
    val list = remember { mutableStateListOf("") }
    Column {
        list.forEachIndexed { index, item ->
            OutlinedTextField(
                value = item,
                onValueChange = {
                    if (index == list.lastIndex && it.isNotEmpty()) {
                        list.add("")
                    } else {
                        list[index] = it
                    }
                },
                modifier = Modifier
                    .fillMaxWidth()
                    .relocate("$index"),
                placeholder = { Text(text = "type anything") }
            )
        }
    }
}
r

rsktash

08/27/2021, 7:15 PM
@Ralston Da Silva How compose determines previous focus with dynamic content? How it restores?
I wrapped my code in a separate Composable . Now focus is not restored. You can pull last commit from repo. There is another issue with onValueChange event. It’s called two times
I achieved what I wanted. I wrapped if-else codes in a separate Composable function and for each TextField set FocusRequester which is created via remember(index){} wrapper. Now it restores the last focus position on recomposition
But I didn’t understand the inner logic of Compose on how it restores focus by default without FocusRequesters
r

Ralston Da Silva

08/27/2021, 9:27 PM
Focus state is stored in Modifier.focusable() which is backed by a lower level Modifier.focusTarget(). FocusRequesters merely point to a focusTarget down the tree. When you call focusRequester.requestFocus() it sends that message to the Modifier.focusTarget. You could have multiple focusRequesters for each focusTarget. Your use-case does not need any focus requester because you are not requesting focus directly. The TextField has a FocusRequester. When you click on a text field, it calls requestFocus() for you. The TextField also has a Modifier.focusable() which makes it focusable. When this focusable gains focus, it retains it's state (across recompositions) as long as it is part of the tree. In your example, you conditionally compose the items based on the index in the list. Because of this you would lose focus when you compose a new item that replaced the TextField that was focused. But if you write your code such that you retain the TextField but just change it's parameters based on it's position in the list, you retain the original TextField, and also retain focus.
r

rsktash

08/27/2021, 9:30 PM
@Ralston Da Silva in the repro I minimized my use case for that reason I didn't use it. In my app I am focusing to the next textField on click done - ime button
r

Ralston Da Silva

08/27/2021, 9:39 PM
Copy code
@Composable
private fun ComposeItem(
  item: String,
  isLastItem: Boolean,
  onValueChange: (String) -> Unit,
  addNew: (String) -> Unit,
  focusRequester: FocusRequester
) {
  if (!isLastItem) {
    OutlinedTextField(
      value = item,
      onValueChange = {
        onValueChange(it)
      },
      modifier = Modifier
        .fillMaxWidth()
        .focusRequester(focusRequester),
      placeholder = { Text(text = "type anything") }
    )
  } else {
    OutlinedTextField(
      value = "",
      onValueChange = {
        onValueChange(it)
        if (it.isNotEmpty()) {
          Log.d("ComposeItem", "onValueChange: $it")
          addNew("")
        }
      },
      placeholder = { Text(text = "type new item") }
    )
  }
}
Let's assume the last index is 3 In this function you create TextField in the else block. When that text field has focus, you add a new item to the list. Now, when this is recomposed, index 3 is not the last index, so you create a new TextField in the if block. This newly created TextField does not have focus.
r

rsktash

08/27/2021, 9:54 PM
@Ralston Da Silva Yes. The issue was due to the conditional composables. How can we restore last focus in another composable? I isn’t needed at this time. But maybe in the future
r

Ralston Da Silva

08/27/2021, 9:57 PM
You can't do this because focus is lost/cleared when the composable is disposed. But if you can always add a focusRequester to the other composable and call focusRequester.requestFocus() to bring it into focus
r

rsktash

08/27/2021, 9:58 PM
Thank you
r

Ralston Da Silva

08/27/2021, 9:58 PM
You're welcome!
r

rsktash

08/27/2021, 9:59 PM
I have another question regarding relocation on keyboard show. Is there a better way to enqueue brintToView action after IME show animation?
r

Ralston Da Silva

08/27/2021, 10:10 PM
Hmm.. not that I know of. What kind of API woudl you like?
something like onAnimationCompleted ?
r

rsktash

08/27/2021, 10:12 PM
May be I’ve to customize this function and pass controller class like
AnimatedInsetState
where we can pass to enqueue actions
I’m using accompanist insets
r

Ralston Da Silva

08/27/2021, 10:14 PM
I'm out of my depth here. Can you post this as a separate question?
r

rsktash

08/27/2021, 10:14 PM
Ok thank you
r

Ralston Da Silva

08/27/2021, 10:15 PM
👍🏼
103 Views