Search code examples
androidkotlinlambdatypescasting

How to get a lambda expression with Any -> Unit to match multiple lambdas with different enum -> Unit?


I created a Dropdown composable that I want to use for different lists of enums. To do this, the Dropdown accepts an onValueChange parameter that is a lambda function with an Any type.

I first tried having onValueChange with a type of (Any) -> Unit and I got a complaint of Type mismatch. Required: (Any) -> Unit. Found: (Color) -> Unit.

I then used typealias Callback<T> = (T) -> Unit and get the same thing with Type mismatch. Required: Callback<Any>. Found: Callback<Color>.

If I cast the lambda to (Any) -> Unit or Callback<Any>, I get an Unchecked cast warning. I can suppress this warning, but I'd rather fix it the "correct" way.

If I set the type of onValueChange to Callback<*>, I no longer get a type error when calling Dropdown, but Dropdown's internal onValueChange call now has Type mismatch with Callback<*>. Required: Nothing. Found: Any

  1. How can I make this work without the type mismatches and unchecked cast warnings?
  2. Why does Kotlin not consider a lambda with a specific typed parameter to be a lambda with an Any parameter? Isn't the whole point of Any for things like this?

Example Uses

@Composable
fun ColorDropdown(color: Color, onColorChange: (Color) -> Unit) {
    val colorOptions = listOf(Color.BLACK, Color.GRAYSCALE, Color.WHITE, Color.MONOCHROME)
    // Type mismatch
    Dropdown(color, options = colorOptions, onValueChange = onColorChange)
}

fun AlignDropdown(color: Align, onAlignChange: Callback<Align>) {
    val alignOptions = listOf(Align.LEFT, Align.CENTER, Align.RIGHT)
    // Unchecked cast
    Dropdown(color, options = colorOptions, onValueChange = onAlignChange as Callback<Any>)
}

Dropdown file

typealias Callback<T> = (T) -> Unit

@Composable
fun Dropdown(value: Any, options: List<Any>, onValueChange: Callback<Any>) {
    var expanded by remember { mutableStateOf(false) }

    Box {
        Button( onClick = { expanded = !expanded } ) { Text("Color: $value") }
        DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false } ) {
            options.forEach { option ->
                DropdownMenuItem(
                    text = { Text(option.toString()) },
                    onClick = {
                        // Type mismatch with Callback<*>
                        // Required: Nothing. Found: Any.
                        onValueChange(option)
                        expanded = false
                    }
                )
            }
        }
    }
}

Solution

  • Your code is conceptually wrong, it is not type-safe - this is why Kotlin disallows it.

    In ColorDropdown you have a lambda which receives a Color and it can work with colors only. Then you want to cast it to (Any) -> Unit, so to a lambda which can receive any arbitrary object. What if we then invoke it passing a String or an Int?

    If your idea is that the lambda always receives one of items passed in options, then you have to make your function generic:

    fun <T> Dropdown(value: T, options: List<T>, onValueChange: Callback<T>) {