DropDownMenu frame drops and lags in Jetpack Compose

1.6k views Asked by At

So I am bit of in a pickle now.

The Begining

I have list of countries and I want to allow the user to pick any of them by showing a drop a down menu, and earlier no jetpack compose way, I had a an extension on View say View.setupDropDown and this inturn loads up a ListPopupWindow and anchors it to the current view, and shows all the items and works perfectly fine without jank or any frame drop.

like

val dropDown = ListPopupWindow(context)
    dropDown.setAdapter(
        ArrayAdapter(
            context,
            android.R.layout.simple_spinner_dropdown_item,
            list.map { itemFormatter(it) })
    )

and show it, or I can use a custom

The Pickle

So now I am building the same experience in Jetpack Compose, and Using DropDownMenu and it loads up all those items in a Column which works fine when Items are less in number, but, when it comes to large number of items like, a list which has more then 100 items, it drops a few frames, and shows the PopUp after a delay.

I looked up insides and tried to replace the Column with LazyColumn by copying in all those files to a sample project but that doest work as Intrinsic measurements is not yet supported for the Subcomposables and it throws and exception and fails.

 DropdownMenu(
        toggle = toggle,
        expanded = showMenu,
        onDismissRequest = { onDismiss() },
    ) {
        options.forEach{ item ->
            DropdownMenuItem(onClick = {
                onDismiss()
            }) {
                Text(text = item)
            }
        }
    }

It works perfectly fine If I apply fixed height and width to the LazyColumn, using the modifier Modifier.height(200.dp).widht(300.dp)

I looked up in issue tracker, and found this issue which was relevant but not same, and the suggestion was to do what I did above.

Not sure what to use in here, as Compose is still new, don't know which component fits the bill.

1

There are 1 answers

1
Aerim On

Update: There is an issue tracker report on this.

You can try the solution inside which also works (if you don't already have a lazy column)

DropdownMenu(
    expanded = expanded,
    onDismissRequest = { expanded = false },
) {
    Box(modifier = Modifier.size(width = 100.dp, height = 300.dp)) {
        LazyColumn {
            items(largeList) { item ->
                DropdownMenuItem(
                    text = { Text(text = item) },
                    onClick = {
                        expanded = false
                    },
                )
            }
        }
    }
}

Previous answer

It may be a little late but I found this post

That worked for me.

Adding the code here for the future:

    @Composable
fun <T> LargeDropdownMenu(
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    label: String,
    notSetLabel: String? = null,
    items: List<T>,
    selectedIndex: Int = -1,
    onItemSelected: (index: Int, item: T) -> Unit,
    selectedItemToString: (T) -> String = { it.toString() },
    drawItem: @Composable (T, Boolean, Boolean, () -> Unit) -> Unit = { item, selected, itemEnabled, onClick ->
        LargeDropdownMenuItem(
            text = item.toString(),
            selected = selected,
            enabled = itemEnabled,
            onClick = onClick,
        )
    },
) {
    var expanded by remember { mutableStateOf(false) }

    Box(modifier = modifier.height(IntrinsicSize.Min)) {
        OutlinedTextField(
            label = { Text(label) },
            value = items.getOrNull(selectedIndex)?.let { selectedItemToString(it) } ?: "",
            enabled = enabled,
            modifier = Modifier.fillMaxWidth(),
            trailingIcon = {
                val icon = expanded.select(Icons.Filled.ArrowDropUp, Icons.Filled.ArrowDropDown)
                Icon(icon, "")
            },
            onValueChange = { },
            readOnly = true,
        )

        // Transparent clickable surface on top of OutlinedTextField
        Surface(
            modifier = Modifier
                .fillMaxSize()
                .padding(top = 8.dp)
                .clip(MaterialTheme.shapes.extraSmall)
                .clickable(enabled = enabled) { expanded = true },
            color = Color.Transparent,
        ) {}
    }

    if (expanded) {
        Dialog(
            onDismissRequest = { expanded = false },
        ) {
            MyTheme {
                Surface(
                    shape = RoundedCornerShape(12.dp),
                ) {
                    val listState = rememberLazyListState()
                    if (selectedIndex > -1) {
                        LaunchedEffect("ScrollToSelected") {
                            listState.scrollToItem(index = selectedIndex)
                        }
                    }

                    LazyColumn(modifier = Modifier.fillMaxWidth(), state = listState) {
                        if (notSetLabel != null) {
                            item {
                                LargeDropdownMenuItem(
                                    text = notSetLabel,
                                    selected = false,
                                    enabled = false,
                                    onClick = { },
                                )
                            }
                        }
                        itemsIndexed(items) { index, item ->
                            val selectedItem = index == selectedIndex
                            drawItem(
                                item,
                                selectedItem,
                                true
                            ) {
                                onItemSelected(index, item)
                                expanded = false
                            }

                            if (index < items.lastIndex) {
                                Divider(modifier = Modifier.padding(horizontal = 16.dp))
                            }
                        }
                    }
                }
            }
        }
    }
}

@Composable
fun LargeDropdownMenuItem(
    text: String,
    selected: Boolean,
    enabled: Boolean,
    onClick: () -> Unit,
) {
    val contentColor = when {
        !enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = ALPHA_DISABLED)
        selected -> MaterialTheme.colorScheme.primary.copy(alpha = ALPHA_FULL)
        else -> MaterialTheme.colorScheme.onSurface.copy(alpha = ALPHA_FULL)
    }

    CompositionLocalProvider(LocalContentColor provides contentColor) {
        Box(modifier = Modifier
            .clickable(enabled) { onClick() }
            .fillMaxWidth()
            .padding(16.dp)) {
            Text(
                text = text,
                style = MaterialTheme.typography.titleSmall,
            )
        }
    }
}

Usage:

var selectedIndex by remember { mutableStateOf(-1) }
LargeDropdownMenu(
    label = "Sample",
    items = listOf("Item 1", "Item 2", "Item 3"),
    selectedIndex = selectedIndex,
    onItemSelected = { index, _ -> selectedIndex = index },
)