0

I would like to create a widget in Jetpack Compose (Desktop) with similar functionality as the JSpinner in Swing, i.e. an editable text field and two buttons that increase/decrease the value in the text field. Also, I would like

  • the value to be validated and to be saved when the spinner loses its focus
  • the buttons not to be skipped in the navigation, so that the user can navigate directly between multiple spinner text fields

After a lot of trial and error I have figured out the following working version, but I wonder if there is a simpler or more elegant way to do this:

@Composable
fun TextFieldSpinner(
    label: @Composable (() -> Unit)?,
    lastText: String,
    validateText: (String) -> Boolean,
    commitText: (String) -> Unit,
    onIncrement: () -> Unit,
    onDecrement: () -> Unit
) {
    val (isEditing, setEditing) = remember { mutableStateOf(false) }

    // intermediateTextFieldValue is only used locally to store the temporary state of the TextField while editing
    val (intermediateTextFieldValue, setIntermediateTextFieldValue) = remember { mutableStateOf(TextFieldValue(lastText))}
    var isError by remember { mutableStateOf(!validateText(lastText)) }

    val resetText = { setIntermediateTextFieldValue(TextFieldValue(lastText)) }

    if (!isEditing && !isError && lastText != intermediateTextFieldValue.text) {
        resetText()
    }

    val onCommit = {
        if (validateText(intermediateTextFieldValue.text)) {
            isError = false
            commitText(intermediateTextFieldValue.text)
        } else {
            isError = true
        }
    }

    val onLeaveTextField = {
        setEditing(false)
        onCommit()
        isError = false
    }

    val onNewFocusState = { newFocusState: Boolean ->
        setEditing(newFocusState)
        if (!newFocusState)
            onCommit()
        }
    }

    TextFieldSpinnerUI(
        label = label,
        value = intermediateTextFieldValue,
        onValueChange = { newTextFieldValue:TextFieldValue ->
            setEditing(true)
            setIntermediateTextFieldValue(newTextFieldValue)

            // eager committing without showing error state
            if (validateText(newTextFieldValue.text)) {
                isError = false
                commitText(newTextFieldValue.text)
            }
        },
        modifier = modifier,
        isError = isError,
        onIncrement = {
            onLeaveTextField()
            onIncrement()
        },
        onDecrement = {
            onLeaveTextField()
            onDecrement()
        },
        onFocusChanged = { state ->
            onNewFocusState(state.isFocused)
        }
    )
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun TextFieldSpinnerUI(
    label: @Composable (() -> Unit)?,
    value: TextFieldValue,
    onValueChange: (TextFieldValue) -> Unit,
    onIncrement: () -> Unit,
    onDecrement: () -> Unit,
    isError: Boolean,
    onFocusChanged: (FocusState) -> Unit
) {
    Row() {
        DecreaseButton(onDecrement)

        TextField(
            label = label,
            value = value,
            singleLine = true,
            isError = isError,
            onValueChange = onValueChange,
            modifier = Modifier.onFocusChanged(onFocusChanged)
        )

        IncreaseButton(onIncrement)
    }
}

@Composable
private fun DecreaseButton(
    onClick: () -> Unit
) {
    IconButton(
        onClick = onClick,
        modifier = Modifier.focusProperties {this.canFocus = false }
    ) {
        Icon(
            imageVector = Icons.Rounded.Remove
        )
    }
}

@Composable
private fun IncreaseButton(
    onClick: () -> Unit
) {
    IconButton(
        onClick = onClick,
        modifier = Modifier.focusProperties { this.canFocus = false }
    ) {
        Icon(
            imageVector = Icons.Rounded.Add
        )
    }
}

In particular, it seems to be hard to have the text field at the same time

  • to be editable
  • to be validated, saved and then recomposed with the new value
  • to have another widget such as the buttons change its value
clamentjohn
  • 3,417
  • 2
  • 18
  • 42
Thomas
  • 101

0 Answers0