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

kotlinforandroid

07/08/2022, 2:53 PM
I am trying to create a list of items similar to a todo list with a checkbox and some text next to it. I also want to have a shadow item that the user can change focus to. Once they type into it, a new "data item" gets created. The problem I encounter is that my shadow item is different from the newly created item. Thus the focus is lost. I am not sure how to correctly implement this. I tried it with
key
and this does sadly not work. I think I know why (the item is not at the same "position" in the composable and thus can't be the same). Code in thread. Any help is appreciated!
Copy code
@JvmInline
value class CheckboxListState(
    val items: List<CheckboxListItem> = emptyList(),
)

data class CheckboxListItem(
    val text: String,
    val checked: Boolean,
)

@Composable
fun CheckboxList(
    modifier: Modifier = Modifier,
    state: CheckboxListState,
    onCheckedChange: (Int, Boolean) -> Unit,
    onTextValueChange: (Int, String) -> Unit,
    onNewItem: () -> Unit,
) {
    val focusManager = LocalFocusManager.current

    Column(modifier = modifier) {
        state.items.forEachIndexed { index, item ->
            key(index) {
                CheckboxListItem(
                    item = item,
                    onCheckedChange = { onCheckedChange(index, it) },
                    onTextValueChange = { onTextValueChange(index, it) },
                    onImeNext = {
                        focusManager.moveFocus(FocusDirection.Down)
                    },
                    imeAction = ImeAction.Next,
                )
            }
        }

        // The shadow item is only visible if the last item contains any text.
        if (state.items.lastOrNull()?.text?.isNotBlank() == true) {
            key(state.items.size) {
                CheckboxListItem(
                    modifier = Modifier.alpha(0.38f),
                    item = CheckboxListItem("", false),
                    onCheckedChange = {},
                    onTextValueChange = {
                        onNewItem()
                    },
                    onImeNext = {},
                )
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CheckboxListItem(
    modifier: Modifier = Modifier,
    item: CheckboxListItem,
    onCheckedChange: (Boolean) -> Unit,
    onTextValueChange: (String) -> Unit,
    imeAction: ImeAction = ImeAction.Default,
    onImeNext: (KeyboardActionScope) -> Unit,
) {
    val textStyle = LocalTextStyle.current.copy(
        textDecoration = if (item.checked) TextDecoration.LineThrough else TextDecoration.None,
    )

    Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
        Box(modifier = Modifier.size(64.dp), contentAlignment = Alignment.Center) {
            Icon(Icons.Default.DragIndicator, contentDescription = "Move item icon")
        }

        Box(modifier = Modifier.size(64.dp), contentAlignment = Alignment.Center) {
            Checkbox(checked = item.checked, onCheckedChange = onCheckedChange)
        }

        TextField(
            modifier = Modifier.weight(1f),
            value = item.text,
            onValueChange = onTextValueChange,
            colors = TextFieldDefaults.textFieldColors(
                containerColor = Color.Transparent,
                focusedIndicatorColor = Color.Transparent,
                unfocusedIndicatorColor = Color.Transparent,
            ),
            singleLine = true,
            readOnly = item.checked,
            keyboardActions = KeyboardActions(onNext = onImeNext),
            keyboardOptions = KeyboardOptions(imeAction = imeAction),
            textStyle = textStyle,
        )
    }
}

@Preview
@Composable
internal fun PreviewCheckboxList() {
    MaterialTheme {
        val items = remember {
            mutableStateListOf(
                CheckboxListItem("Hello, A!", false),
                CheckboxListItem("Hello, B!", true),
                CheckboxListItem("Hello, C!", false),
                CheckboxListItem("Hello, D!", true),
            )
        }

        CheckboxList(
            state = CheckboxListState(items = items),
            onCheckedChange = { index, value ->
                items[index] = items[index].copy(checked = value)
            },
            onTextValueChange = { index, value ->
                items[index] = items[index].copy(text = value)
            },
        ) {
            items.add(CheckboxListItem("", false))
        }
    }
}
s

Sean Proctor

07/08/2022, 4:17 PM
Instead of using FocusManager, you can use FocusRequesters like: https://stackoverflow.com/a/66819665/45364 That would allow you request focus when the element is changed.
k

kotlinforandroid

07/08/2022, 5:17 PM
I think the problem lies in the shadow textfield not being the same instance as the last one even if the text is empty. The solution proposed in the SO link is what I am doing right now, using
ImeAction.Next
to change focus to the next field.
s

Sean Proctor

07/08/2022, 5:37 PM
It's similar, but not the same. In the answer I linked to, the FocusRequester is manually set, he doesn't use FocusManager.
I think you could use the same FocusRequester on the new element as the placeholder had previously.
I think there is a bug in that answer because it doesn't remember the focusRequesters list. I think you'd want to remember that list and grow it when a new element is added.
4 Views