In our app, we need to display a paginated list of options inside a drop-down menu. Since the list of data is lazy, the size is not constant and thus can cause crashes if the nested PaginatedLazyColumn() composable isn't assigned constant size values. This obviously looks bad, so my question is, how should I approach this?
Should I somehow calculate the width of the longest data string (that is available at page 0 of the data payload, since if a longer string comes along, it can be truncated) and set that as the dropdown's width, while keeping the height at a “comfortable” DP size? If so, how could I do that?
I have created the following composable that represents a drop-down in our app:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LyraDropDown(
modifier: Modifier = Modifier,
data: List<String>,
defaultSelectionIndex: Int = 0,
label: String,
onClick: (Int) -> Unit,
paginationCallback: () -> Unit = {},
itemContent: @Composable (String) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
var selectedIndex by remember { mutableIntStateOf(defaultSelectionIndex) }
val listWithDefaultOption = data.toMutableList()
listWithDefaultOption.add(0, "No selection")
ExposedDropdownMenuBox(
modifier = modifier.background(
colorResource(id = R.color.white),
shape = RoundedCornerShape(8.dp)
),
expanded = expanded, onExpandedChange = { expanded = !expanded }) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.menuAnchor()
.background(colorResource(id = R.color.white), shape = RoundedCornerShape(8.dp)),
readOnly = true,
label = {
Text(label, fontSize = 12.sp)
},
value = listWithDefaultOption[selectedIndex],
textStyle = TextStyle(fontSize = 12.sp),
onValueChange = { },
trailingIcon = {
Icon(
painter = painterResource(id = if (!expanded) R.drawable.ic_chev_down else R.drawable.ic_chev_up),
contentDescription = null,
modifier = Modifier
.size(24.dp)
.padding(4.dp),
tint = colorResource(id = R.color.gray_700)
)
},
shape = RoundedCornerShape(8.dp),
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(
unfocusedContainerColor = colorResource(id = R.color.white),
focusedContainerColor = colorResource(id = R.color.white),
focusedBorderColor = colorResource(id = R.color.indigo_500),
unfocusedBorderColor = colorResource(id = R.color.gray_200),
focusedLabelColor = colorResource(id = R.color.blue_500),
unfocusedLabelColor = colorResource(id = R.color.black)
)
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
},
modifier = Modifier
.background(colorResource(id = R.color.white))
.width(200.dp)
.height(300.dp),
) {
PaginatedLazyColumn(
modifier = Modifier
.width(200.dp)
.height(300.dp),
items = listWithDefaultOption,
itemKey = { UUID.randomUUID() },
itemContentIndexed = { selectionOption, index ->
DropdownMenuItem(
onClick = {
selectedIndex = index
expanded = false
if (index != 0) onClick(index - 1) // account for hardcoded option
},
text = {
itemContent(selectionOption)
})
},
loadingItem = { LyraCircularProgressIndicator() },
contentWhenEmpty = { }) {
paginationCallback()
}
}
}
}
The PaginatedLazyColumn
is defined as:
@Composable
internal fun <T> PaginatedLazyColumn(
modifier: Modifier = Modifier,
loading: Boolean = false,
listState: LazyListState = rememberLazyListState(),
items: List<T>,
itemKey: (T) -> Any,
itemContentIndexed: @Composable (T, Int) -> Unit,
loadingItem: @Composable () -> Unit,
contentWhenEmpty: @Composable () -> Unit,
loadMore: () -> Unit
) {
val reachedBottom: Boolean by remember { derivedStateOf { listState.reachedBottom() } }
// load more if scrolled to bottom
LaunchedEffect(reachedBottom) {
if (reachedBottom && !loading) loadMore()
}
LazyColumn(modifier = modifier, state = listState) {
itemsIndexed(
items = items,
key = { _, item -> itemKey(item) }
) { index, item ->
itemContentIndexed(item, index)
}
if (items.isEmpty()) {
item {
contentWhenEmpty()
}
}
if (loading) {
item {
loadingItem()
}
}
}
}
private fun LazyListState.reachedBottom(buffer: Int = 1): Boolean {
val lastVisibleItem = this.layoutInfo.visibleItemsInfo.lastOrNull()
return lastVisibleItem?.index != 0 && lastVisibleItem?.index == this.layoutInfo.totalItemsCount - buffer
}