electrolobzik
02/15/2024, 1:02 PMcoil
in a lazyList
I see lags during scrolling because image needs some time to load. Is it possible to somehow preload a couple of items in direction of scrolling to avoid that lags? I was not able to find any API for that.ursus
02/15/2024, 1:17 PMelectrolobzik
02/15/2024, 1:19 PMelectrolobzik
02/15/2024, 1:20 PMursus
02/15/2024, 1:27 PMelectrolobzik
02/15/2024, 1:30 PMursus
02/15/2024, 1:33 PMelectrolobzik
02/15/2024, 1:34 PMursus
02/15/2024, 1:34 PMursus
02/15/2024, 1:35 PMelectrolobzik
02/15/2024, 1:35 PMelectrolobzik
02/15/2024, 1:35 PMFyodor Danilov
02/15/2024, 1:36 PMFyodor Danilov
02/15/2024, 1:36 PMursus
02/15/2024, 1:37 PMelectrolobzik
02/15/2024, 1:37 PMshikasd
02/15/2024, 2:16 PMlayoutInfo
.
Prefetching items is somewhat wasteful in this scenario, as you end up doing the work that AsyncImage
does ahead of timeelectrolobzik
02/15/2024, 2:23 PMshikasd
02/15/2024, 2:30 PMelectrolobzik
02/15/2024, 2:36 PMshikasd
02/15/2024, 2:40 PMLazyColumn
layout to measure the future items, I think
That's what prefetch in LazyColumn
does.electrolobzik
02/15/2024, 2:43 PMshikasd
02/15/2024, 2:46 PMelectrolobzik
02/15/2024, 3:01 PMforceRemeasure
the correct way to change the size? Or should I have some state value measuredHeight
in the scope and change the modifier via something like
.run {
if (measuredHeight != null) {
height(measuredHeight)
} else {
aspectRatio(3f/4)
}
}
?shikasd
02/15/2024, 3:54 PMaspectRatio
does this thing for you already, iircelectrolobzik
02/15/2024, 5:14 PMshikasd
02/15/2024, 5:57 PMelectrolobzik
02/15/2024, 6:19 PMMedia
composable provided by com.github.fengdai.compose:media
which is a compose wrapper around androidx.media3. If I don’t specify any size constrain on height (only fillMaxWidth
), the Media
height in the LazyColumn
is 0 even after loading. But if used outside of the LazyColumn, In a Box, for example it loads and shows video with same settings. it is based on AndroidView around Exoplayer. It has pretty complex logic around video loading. As I understood the main point is here (image below). Probably it is not the best approach.shikasd
02/15/2024, 6:20 PMelectrolobzik
02/15/2024, 6:23 PMshikasd
02/15/2024, 6:40 PMshikasd
02/15/2024, 6:41 PMelectrolobzik
02/15/2024, 6:53 PMTimo Drick
02/18/2024, 11:51 AMTimo Drick
02/18/2024, 11:54 AMctx.imageLoader.enqueue(
ImageRequest.Builder(ctx)
.data(imageUrl)
.apply { metrics?.let { size(metrics.widthPixels, metrics.heightPixels) } }
.build()
)
As you can see you can also provide a size for the decoded image i think. Not sure maybe it will decode the image also. But than it is decompressed in the memory and there is not much room for cached imags.electrolobzik
02/18/2024, 1:14 PMTimo Drick
02/18/2024, 1:19 PMTimo Drick
02/18/2024, 1:24 PMinline fun <T> LazyListScope.itemsFlow(
flow2StateList: FlowListCollector<T>,
crossinline itemContent: @Composable LazyItemScope.(item: FlowState<T>) -> Unit
) = itemsIndexed(flow2StateList.list) { index, item ->
flow2StateList.requestListSize(index + 1 + 10)
itemContent(this, item)
}
fun <T> Flow<T>.stateList() = FlowListCollector(this)
sealed class FlowState<out T> {
object Loading : FlowState<Nothing>()
object Empty : FlowState<Nothing>()
class Error(val error: Throwable): FlowState<Nothing>()
class Item<T>(val value: T): FlowState<T>()
}
class FlowListCollector<T>(
private val flow: Flow<T>
): CoroutineScope {
private val job = Job()
override val coroutineContext = Dispatchers.Main + job
val list: SnapshotStateList<FlowState<T>> = mutableStateListOf()
private val listItemsChannel = Channel<Int>(Channel.CONFLATED)
private val requestItemsChannel = Channel<Int>(Channel.CONFLATED)
private val retryChannel = Channel<Boolean>(Channel.CONFLATED)
private var requestedListSize = 1
private var listSize = 0
init {
list.add(FlowState.Loading) // set last list item to loading
launch {
var inProgress = true
log("$this - Start collecting for $flow")
if (requestedListSize <= 0) {
requestedListSize = requestItemsChannel.receive()
}
flow.buffer(5)
.retryWhen { cause, _ ->
log("Retry", cause)
if (inProgress) {
list.removeLast()
}
list.add(FlowState.Error(cause)) // Add error item to end of list
listItemsChannel.send(list.size)
inProgress = true
return@retryWhen retryChannel.receive()
}.collect {
if (inProgress) {
list.removeLast()
inProgress = false
}
list.add(FlowState.Item(it))
listSize++
listItemsChannel.send(list.size)
if (listSize >= requestedListSize) {
log("Wait until new items needed. List size: $listSize requested: $requestedListSize")
requestedListSize = requestItemsChannel.receive()
}
}
if (inProgress) {
inProgress = false
list.removeLast()
if (list.isEmpty()) {
list.add(FlowState.Empty)
}
}
log("$this - collection ready")
}
}
suspend fun preload(requestSize: Int) {
if (job.isActive) {
requestItemsChannel.send(requestSize)
//Wait until preload is ready
while (listSize < requestSize) {
val newSize = listItemsChannel.receive()
log("new size: $newSize list size: ${listSize}")
}
}
}
fun requestListSize(requestSize: Int) {
if (job.isActive) {
launch {
log("Request items. List size: $listSize requested: $requestSize")
if (listSize < requestSize) {
log("Send request")
requestItemsChannel.send(requestSize)
}
}
}
}
fun retry() {
launch {
retryChannel.send(true)
}
}
protected fun finalize() {
job.cancel()
requestItemsChannel.cancel()
retryChannel.cancel()
log("$this - Channel and job canceled")
}
}