1

Currently, I have a DropDown implemented with particular types. I would like to make that drop-down menu generic so I can reuse it everywhere with custom objects and remove boilerplate code.

Current Implementation

    @Composable
fun SingleLineDropDown(optionsFieldState: OptionsFieldState<String>, hintText: String, isEditable: Boolean = true) {
    var expanded by remember { mutableStateOf(false) }

    var textFieldSize by remember { mutableStateOf(Size.Zero) }

    val icon = if (expanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown

    val modifier = if (isEditable) {
        Modifier.clickable { expanded = !expanded }
    } else Modifier

    Box {
        EditableRow(
            value = optionsFieldState.value ?: "",
            onValueChange = { optionsFieldState.value = it },
            enabled = false,
            error = optionsFieldState.error.orEmpty(),
            modifier = modifier
                .fillMaxWidth()
                .onGloballyPositioned { coordinates ->
                    //This value is used to assign to the DropDown the same width
                    textFieldSize = coordinates.size.toSize()
                },
            hintText = hintText,
            trailingIcon = {
                Icon(
                    icon, null, modifier
                )
            },
            colors = TextFieldDefaults.outlinedTextFieldColors(
                disabledTextColor = if (isEditable) MaterialTheme.colors.onBackground else Color.LightGray
            )
        )
        if (isEditable) {
            DropdownMenu(
                expanded = expanded,
                onDismissRequest = { expanded = false },
                modifier = Modifier.width(with(LocalDensity.current) { textFieldSize.width.toDp() })
            ) {
                optionsFieldState.options.forEach { option ->
                    DropdownMenuItem(onClick = {
                        optionsFieldState.value = option
                        expanded = !expanded
                    }) {
                        Text(text = option)
                    }
                }
            }
        }
    }
}

Caller function

SingleLineDropDown(
    optionsFieldState = state.province,
    hintText = stringResource(id = R.string.province_territory),
    isEditable = state.isAddressEditable
)

OptionFiledState is custom state for compose UI

open class OptionsFieldState<T>(
    initialValue: T?,
    options: List<T> = listOf(),
    validators: List<Validator> = listOf(),
    onValueChanged: () -> Unit = {},
)
Arpit Patel
  • 7,212
  • 5
  • 56
  • 67

1 Answers1

1

If you want your drop-down menu to be applicable to generics custom objects, you can change your SingleLineDropDown function with generics, along side one extra parameters for the function to make it work

Follow this example to understand how it can be done:

@Composable
fun <T> SingleLineDropDown(
    optionsFieldState: OptionsFieldState<T>,
    hintText: String,
    isEditable: Boolean = true,
    displayText: @Composable (T) -> Unit // Composable function to display the selected option
) {
    // Rest of your code...

    Box {
        EditableRow(
            // Rest of your code...
        )
        if (isEditable) {
            DropdownMenu(
                // Rest of your code...
            ) {
                optionsFieldState.options.forEach { option ->
                    DropdownMenuItem(onClick = {
                        optionsFieldState.value = option
                        expanded = !expanded
                    }) {
                        displayText(option) // Call the displayText function to show the selected option
                    }
                }
            }
        }
    }
}

Now you can use it the following way:

SingleLineDropDown(
    optionsFieldState = state.province,
    hintText = stringResource(id = R.string.province_territory),
    isEditable = state.isAddressEditable
) { option ->
    // Customize how the selected option is displayed
    Text(text = option.name) // Assuming the custom object has a "name" property
}

Update You can also provide a default value for the generic to be used in the EditableRow, something like that:

@Composable
fun <T> SingleLineDropDown(
    optionsFieldState: OptionsFieldState<T>,
    hintText: String,
    isEditable: Boolean = true,
    displayText: @Composable (T) -> Unit,
    defaultOptionValue : T
) {
    // Rest of your code...

    Box {
        EditableRow(
            value = optionsFieldState.value ?: defaultOptionValue,
            onValueChange = { optionsFieldState.value = it },
            enabled = false,
            error = optionsFieldState.error.orEmpty(),
            modifier = modifier
                .fillMaxWidth()
                .onGloballyPositioned { coordinates ->
                    //This value is used to assign to the DropDown the same width
                    textFieldSize = coordinates.size.toSize()
                },
            hintText = hintText,
            trailingIcon = {
                Icon(
                    icon, null, modifier
                )
            },
            colors = TextFieldDefaults.outlinedTextFieldColors(
                disabledTextColor = if (isEditable) MaterialTheme.colors.onBackground else Color.LightGray
            )
        )
       // Rest of your code...
Younes Charfaoui
  • 1,059
  • 3
  • 10
  • 20
  • Thank you for the answer and I also have some logic in `EditableRow` to set that custom object in `OptionFieldState` if you can provide that as well would be great help. – Arpit Patel May 22 '23 at 16:16
  • I am not able to know what is `optionFieldState.error` and `optionFieldState.value`, and I think it is the same thing as the previous ones, maybe just add some default value to the EditableRow when passing the data! – Younes Charfaoui May 23 '23 at 05:38
  • Just updated the code; kindly check. – Younes Charfaoui May 23 '23 at 05:43