Skip to content

Instantly share code, notes, and snippets.

@bmc08gt
Last active March 13, 2023 04:42
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmc08gt/3554d17c1a4f98accc370a8fbfa5f226 to your computer and use it in GitHub Desktop.
Save bmc08gt/3554d17c1a4f98accc370a8fbfa5f226 to your computer and use it in GitHub Desktop.
A StaggeredVerticalGrid with support for lazyPagingItems in Jetpack Compose
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val size = 200.dp.coerceAtMost(maxWidth / 2) // 200.dp unless screen is for ants; then split in half
LazyStaggeredVerticalGrid(
modifier = Modifier.matchParentSize(),
cells = GridCells.Adaptive(size),
) {
items(media) { item ->
MediaItem(media = item, actioner = actioner)
}
}
}
@Composable
private fun MediaItem(
media: Media?,
modifier: Modifier = Modifier,
actioner: (PhotoDiscoveryAction) -> Unit
) {
NetworkImage(
url = media?.thumbnailUrl.orEmpty(),
modifier = modifier,
contentScale = ContentScale.FillBounds,
contentDescription = media?.caption
)
}
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.paging.PagingData
import androidx.paging.PagingDataDiffer
import androidx.paging.compose.LazyPagingItems
import kotlinx.coroutines.flow.Flow
@ExperimentalFoundationApi
interface LazyStaggeredGridScope {
/**
* Adds a single item to the scope.
*
* @param content the content of the item
*/
fun item(key: Any? = null, content: @Composable LazyItemScope.() -> Unit)
/**
* Adds a [count] of items.
*
* @param count the items count
* @param itemContent the content displayed by a single item
*/
fun items(count: Int, key: ((index: Int) -> Any)? = null, itemContent: @Composable LazyItemScope.(index: Int) -> Unit)
}
/**
* Adds a list of items.
*
* @param items the data list
* @param itemContent the content displayed by a single item
*/
@ExperimentalFoundationApi
inline fun <T> LazyStaggeredGridScope.items(
items: List<T>,
crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
) = items(items.size) {
itemContent(items[it])
}
/**
* Adds a list of items where the content of an item is aware of its index.
*
* @param items the data list
* @param itemContent the content displayed by a single item
*/
@ExperimentalFoundationApi
inline fun <T> LazyStaggeredGridScope.itemsIndexed(
items: List<T>,
crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
) = items(items.size) {
itemContent(it, items[it])
}
/**
* Adds an array of items.
*
* @param items the data array
* @param itemContent the content displayed by a single item
*/
@ExperimentalFoundationApi
inline fun <T> LazyStaggeredGridScope.items(
items: Array<T>,
crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
) = items(items.size) {
itemContent(items[it])
}
/**
* Adds an array of items where the content of an item is aware of its index.
*
* @param items the data list
* @param itemContent the content displayed by a single item
*/
@ExperimentalFoundationApi
inline fun <T> LazyStaggeredGridScope.itemsIndexed(
items: Array<T>,
crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
) = items(items.size) {
itemContent(it, items[it])
}
/**
* Adds the [LazyPagingItems] and their content to the scope. The range from 0 (inclusive) to
* [LazyPagingItems.itemCount] (exclusive) always represents the full range of presentable items,
* because every event from [PagingDataDiffer] will trigger a recomposition.
*
*
* @param lazyPagingItems the items received from a [Flow] of [PagingData].
* @param itemContent the content displayed by a single item. In case the item is `null`, the
* [itemContent] method should handle the logic of displaying a placeholder instead of the main
* content displayed by an item which is not `null`.
*/
@ExperimentalFoundationApi
inline fun <T : Any> LazyStaggeredGridScope.items(
lazyPagingItems: LazyPagingItems<T>,
crossinline itemContent: @Composable LazyItemScope.(value: T?) -> Unit
) = items(lazyPagingItems.itemCount) { index ->
itemContent(lazyPagingItems.getAsState(index).value)
}
/**
* Adds the [LazyPagingItems] and their content to the scope where the content of an item is
* aware of its local index. The range from 0 (inclusive) to [LazyPagingItems.itemCount] (exclusive)
* always represents the full range of presentable items, because every event from
* [PagingDataDiffer] will trigger a recomposition.
*
*
* @param lazyPagingItems the items received from a [Flow] of [PagingData].
* @param itemContent the content displayed by a single item. In case the item is `null`, the
* [itemContent] method should handle the logic of displaying a placeholder instead of the main
* content displayed by an item which is not `null`.
*/
@ExperimentalFoundationApi
inline fun <T : Any> LazyStaggeredGridScope.itemsIndexed(
lazyPagingItems: LazyPagingItems<T>,
crossinline itemContent: @Composable LazyItemScope.(index: Int, value: T?) -> Unit
) {
items(lazyPagingItems.itemCount) { index ->
itemContent(index, lazyPagingItems.getAsState(index).value)
}
}
@ExperimentalFoundationApi
@Composable
fun LazyStaggeredVerticalGrid(
cells: GridCells,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
content: LazyStaggeredGridScope.() -> Unit
) {
val scope = LazyStaggeredVerticalGridImpl()
scope.apply(content)
when (cells) {
is GridCells.Fixed ->
BoxWithConstraints(
modifier = modifier
) {
FixedStaggeredVerticalLazyGrid(
nColumns = cells.count,
maxColumnWidth = maxWidth,
state = state,
contentPadding = contentPadding,
scope = scope
)
}
is GridCells.Adaptive ->
BoxWithConstraints(
modifier = modifier
) {
val nColumns = maxOf((maxWidth / cells.minSize).toInt(), 1)
FixedStaggeredVerticalLazyGrid(
nColumns = nColumns,
maxColumnWidth = maxWidth,
state = state,
contentPadding = contentPadding,
scope = scope
)
}
}
}
@ExperimentalFoundationApi
@Composable
private fun FixedStaggeredVerticalLazyGrid(
nColumns: Int,
maxColumnWidth: Dp,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
scope: LazyStaggeredVerticalGridImpl,
) {
val rows = (scope.totalSize + nColumns - 1) / nColumns
LazyColumn(
modifier = modifier,
state = state,
contentPadding = contentPadding
) {
item {
Row {
for (columnIndex in 0 until nColumns) {
Column {
for (rowIndex in 0 until rows) {
val itemIndex = rowIndex * nColumns + columnIndex
if (itemIndex < scope.totalSize) {
Box(modifier = Modifier.width(maxColumnWidth / nColumns)) {
scope.contentFor(itemIndex, this@item).invoke()
}
} else {
Spacer(modifier = Modifier.weight(1f, fill = true))
}
}
}
}
}
}
}
}
private class IntervalContent(
val key: ((index: Int) -> Any)?,
val content: LazyItemScope.(index: Int) -> @Composable() () -> Unit
)
@ExperimentalFoundationApi
class LazyStaggeredVerticalGridImpl : LazyStaggeredGridScope {
private val intervals = IntervalList<IntervalContent>()
val totalSize get() = intervals.totalSize
fun contentFor(index: Int, scope: LazyItemScope): @Composable () -> Unit {
val interval = intervals.intervalForIndex(index)
val localIntervalIndex = index - interval.startIndex
return interval.content.content.invoke(scope, localIntervalIndex)
}
override fun item(key: Any?, content: @Composable LazyItemScope.() -> Unit) {
intervals.add(
1,
IntervalContent(
key = if (key != null) { _: Int -> key } else null,
content = { @Composable { content() } }
)
)
}
override fun items(count: Int, key: ((index: Int) -> Any)?, itemContent: @Composable LazyItemScope.(index: Int) -> Unit) {
intervals.add(
count,
IntervalContent(
key = key,
content = { index -> @Composable { itemContent(index) } }
)
)
}
}
internal class IntervalHolder<T>(
val startIndex: Int,
val size: Int,
val content: T
)
internal class IntervalList<T> {
private val intervals = mutableListOf<IntervalHolder<T>>()
internal var totalSize = 0
private set
fun add(size: Int, content: T) {
if (size == 0) {
return
}
val interval = IntervalHolder(
startIndex = totalSize,
size = size,
content = content
)
totalSize += size
intervals.add(interval)
}
fun intervalForIndex(index: Int) =
if (index < 0 || index >= totalSize) {
throw IndexOutOfBoundsException("Index $index, size $totalSize")
} else {
intervals[findIndexOfHighestValueLesserThan(intervals, index)]
}
/**
* Finds the index of the [list] which contains the highest value of [IntervalHolder.startIndex]
* that is less than or equal to the given [value].
*/
private fun findIndexOfHighestValueLesserThan(list: List<IntervalHolder<T>>, value: Int): Int {
var left = 0
var right = list.lastIndex
while (left < right) {
val middle = (left + right) / 2
val middleValue = list[middle].startIndex
if (middleValue == value) {
return middle
}
if (middleValue < value) {
left = middle + 1
// Verify that the left will not be bigger than our value
if (value < list[left].startIndex) {
return middle
}
} else {
right = middle - 1
}
}
return left
}
}
@lizhuogui
Copy link

Poor performance when the amount of data is large.
You seem to put a large single row in the lazycolum and it's not lazy.

@bmc08gt
Copy link
Author

bmc08gt commented Jul 23, 2021

Yep it was soon discovered after that, that that was the case after talking with the Compose team. There isn’t a performant lazy enabled solution at this time unfortunately.

@suyie001
Copy link

lazyPagingItems.getAsState(index).value got an error. How can I fix it ?Unresolved reference: getAsState

@ahmed-shehataa
Copy link

getAsState not found

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment