rsktash

    rsktash

    1 year ago
    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 TextFieldhttps://github.com/rustamsmax/compose-report
    @Ralston Da Silva
    How the focus restoration works?
    r

    Ralston Da Silva

    1 year ago
    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?
    rsktash

    rsktash

    1 year ago
    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

    1 year ago
    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:
    @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:
    @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") }
                )
            }
        }
    }
    rsktash

    rsktash

    1 year ago
    @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

    1 year ago
    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.
    rsktash

    rsktash

    1 year ago
    @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

    1 year ago
    @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.
    rsktash

    rsktash

    1 year ago
    @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

    1 year ago
    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
    rsktash

    rsktash

    1 year ago
    Thank you
    r

    Ralston Da Silva

    1 year ago
    You're welcome!
    rsktash

    rsktash

    1 year ago
    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

    1 year ago
    Hmm.. not that I know of. What kind of API woudl you like?
    something like onAnimationCompleted ?
    rsktash

    rsktash

    1 year ago
    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

    1 year ago
    I'm out of my depth here. Can you post this as a separate question?
    rsktash

    rsktash

    1 year ago
    Ok thank you
    r

    Ralston Da Silva

    1 year ago
    👍🏼