Skip to content

Instantly share code, notes, and snippets.

@ThePromoter
Last active February 23, 2023 22:58
Show Gist options
  • Save ThePromoter/535620100cd322990a1e808b29c70c07 to your computer and use it in GitHub Desktop.
Save ThePromoter/535620100cd322990a1e808b29c70c07 to your computer and use it in GitHub Desktop.
Prefetching lazy list items with coil
interface CoilPreloaderModelProvider<U> {
fun getPreloadItems(position: Int): List<U>
fun getPreloadRequest(item: U): ImageRequest?
}
class ItemLazyListPreloader(
private val items: List<Item>
) : CoilPreloader.PreloadModelProvider<String> {
override fun getPreloadItems(position: Int): List<String> {
if (position < 0 || position >= items.size) return emptyList()
return when (val item = items.getOrNull(position)) {
null -> emptyList()
else -> listOf(item.imageUrl)
}
}
override fun getPreloadRequest(itemUrl: String): ImageRequest =
ImageRequest.Builder(context).data(imageUrl)
}
@Composable fun <T> LazyListPreloader(
listState: LazyListState,
preloadModelProvider: CoilPreloaderModelProvider<T>,
maxPreload: Int = 1
) {
var lastEnd by remember { mutableStateOf(0) }
var lastStart by remember { mutableStateOf(0) }
var lastFirstVisible by remember { mutableStateOf(-1) }
var totalItemCount by remember { mutableStateOf(0) }
var wasIncreasing by remember { mutableStateOf(true) }
val preloadQueue = ArrayDeque<Disposable>(maxPreload + 1)
val context = LocalContext.current
val imageLoader = context.imageLoader
val scope = rememberCoroutineScope()
val cancelAll = { preloadQueue.forEach { it.dispose() } }
DisposableEffect(listState) {
scope.launch {
snapshotFlow { Triple(listState.firstVisibleItemIndex, listState.layoutInfo.visibleItemsInfo.size, listState.layoutInfo.totalItemsCount) }
.mapNotNull { (firstVisible, visibleCount, totalCount) ->
if (totalItemCount == 0 && totalCount == 0) return@mapNotNull null
totalItemCount = totalCount
val (start, isIncreasing) = when {
firstVisible > lastFirstVisible -> firstVisible + visibleCount to true
firstVisible < lastFirstVisible -> firstVisible to false
else -> return@mapNotNull null
}
lastFirstVisible = firstVisible
return@mapNotNull start to isIncreasing
}
.map { (start, isIncreasing) ->
if (wasIncreasing != isIncreasing) {
wasIncreasing = isIncreasing
cancelAll()
}
return@map start to start + (maxPreload.takeIf { isIncreasing } ?: -maxPreload)
}
.map { (from, to) ->
val isIncreasing = from < to
val (start, end) = when (isIncreasing) {
true -> from.coerceAtLeast(lastEnd).coerceIn(0, totalItemCount) to to.coerceAtMost(totalItemCount)
false -> to.coerceIn(0, totalItemCount) to from.coerceIn(lastStart, totalItemCount)
}
val itemsToLoad = when (isIncreasing) {
true -> (start until end).flatMap { preloadModelProvider.getPreloadItems(it) }
false -> (end - 1 downTo start).flatMap { preloadModelProvider.getPreloadItems(it) }
}
lastStart = start
lastEnd = end
return@map itemsToLoad to isIncreasing
}
.collectLatest { (items, isIncreasing) ->
for (item in items.takeIf { isIncreasing } ?: items.reversed()) {
val imageRequest = preloadModelProvider.getPreloadRequest(item)
if (imageRequest != null) preloadQueue.offer(imageLoader.enqueue(imageRequest))
}
}
}
onDispose { cancelAll() }
}
}
@Composable fun MyListComponent(items: List<Item>) {
val listState = rememberLazyListState()
LazyListPreloader(
listState = listState,
preloadModelProvider = ItemLazyListPreloader(items),
maxPreload = 5
)
LazyColumn(state = listState) {
...
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment