From 025d3c7a96de6cd62b01b64f666807d3d5777cd8 Mon Sep 17 00:00:00 2001 From: Fares Alhassen Date: Thu, 25 Jun 2026 04:54:06 -0700 Subject: [PATCH] Fix memory leak in Glide Compose integration by cancelling preloads on disposal. PiperOrigin-RevId: 937910935 --- .../RememberGlidePreloadingDataTest.kt | 415 ++++++++++-------- .../glide/integration/compose/Preload.kt | 215 ++++----- .../com/bumptech/glide/ListPreloader.java | 2 +- 3 files changed, 354 insertions(+), 278 deletions(-) diff --git a/integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/RememberGlidePreloadingDataTest.kt b/integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/RememberGlidePreloadingDataTest.kt index f34309b515..87dcc5baa8 100644 --- a/integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/RememberGlidePreloadingDataTest.kt +++ b/integration/compose/src/androidTest/java/com/bumptech/glide/integration/compose/RememberGlidePreloadingDataTest.kt @@ -11,8 +11,11 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.platform.testTag @@ -25,217 +28,281 @@ import androidx.test.core.app.ApplicationProvider import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder import com.bumptech.glide.integration.compose.test.GlideComposeRule +import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.target.Target import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test class RememberGlidePreloadingDataTest { - private val context: Context = ApplicationProvider.getApplicationContext() - @get:Rule val glideComposeRule = GlideComposeRule() - - @Test - fun rememberGlidePreloadingData_withoutScroll_preloadsNextItem() { - glideComposeRule.setContent { - val preloadingData = rememberOneItemAtATimePreloadingData() - - LazyRow(modifier = Modifier.testTag(listTestTag)) { - items(preloadingData.size) { index -> - preloadingData.triggerPreload(index) - GlideImage( - model = model, - contentDescription = imageContentDescription(index), - Modifier.fillParentMaxWidth(), - ) - } - } - } + private val context: Context = ApplicationProvider.getApplicationContext() + @get:Rule val glideComposeRule = GlideComposeRule() - assertThatModelIsInMemoryCache(preloadModels[1]) - } + @Test + fun rememberGlidePreloadingData_withoutScroll_preloadsNextItem() { + glideComposeRule.setContent { + val preloadingData = rememberOneItemAtATimePreloadingData() - @Test - fun glideLazyListPreloader_onScroll_preloadsAheadInDirectionOfScroll() { - glideComposeRule.setContent { - val preloadingData = rememberOneItemAtATimePreloadingData() - LazyRow(modifier = Modifier.testTag(listTestTag)) { - items(preloadingData.size) { index -> - preloadingData.triggerPreload(index) - GlideImage( - model = model, - contentDescription = imageContentDescription(index), - Modifier.fillParentMaxWidth(), - ) - } - } + LazyRow(modifier = Modifier.testTag(listTestTag)) { + items(preloadingData.size) { index -> + preloadingData.triggerPreload(index) + GlideImage( + model = model, + contentDescription = imageContentDescription(index), + Modifier.fillParentMaxWidth(), + ) } + } + } - val scrollToIndex = 1 - glideComposeRule.onNode(hasTestTag(listTestTag)).performScrollToIndex(scrollToIndex) + assertThatModelIsInMemoryCache(preloadModels[1]) + } - assertThatModelIsInMemoryCache(preloadModels[2]) + @Test + fun glideLazyListPreloader_onScroll_preloadsAheadInDirectionOfScroll() { + glideComposeRule.setContent { + val preloadingData = rememberOneItemAtATimePreloadingData() + LazyRow(modifier = Modifier.testTag(listTestTag)) { + items(preloadingData.size) { index -> + preloadingData.triggerPreload(index) + GlideImage( + model = model, + contentDescription = imageContentDescription(index), + Modifier.fillParentMaxWidth(), + ) + } + } } - @Test - fun glideLazyListPreloader_withHeaderItem_onScroll_doesNotCrash() { - glideComposeRule.setContent { - val preloadingData = rememberOneItemAtATimePreloadingData() - - LazyRow(modifier = Modifier.testTag(listTestTag)) { - item { Text(text = "Header") } - items(preloadingData.size) { index -> - preloadingData.triggerPreload(index) - GlideImage( - model = model, - contentDescription = imageContentDescription(index), - Modifier.fillParentMaxWidth(), - ) - } - } - } + val scrollToIndex = 1 + glideComposeRule.onNode(hasTestTag(listTestTag)).performScrollToIndex(scrollToIndex) + + assertThatModelIsInMemoryCache(preloadModels[2]) + } + + @Test + fun glideLazyListPreloader_withHeaderItem_onScroll_doesNotCrash() { + glideComposeRule.setContent { + val preloadingData = rememberOneItemAtATimePreloadingData() - // Scroll to the 0th image, accounting for the first header item. - val scrollToIndex = 1 - glideComposeRule.onNode(hasTestTag(listTestTag)).performScrollToIndex(scrollToIndex) - // Make sure the next image, the 1th, is in memory due to preloading. - assertThatModelIsInMemoryCache(preloadModels[1]) + LazyRow(modifier = Modifier.testTag(listTestTag)) { + item { Text(text = "Header") } + items(preloadingData.size) { index -> + preloadingData.triggerPreload(index) + GlideImage( + model = model, + contentDescription = imageContentDescription(index), + Modifier.fillParentMaxWidth(), + ) + } + } } - @Test - fun glideLazyListPreloader_whenDataChanges_onScroll_preloadsUpdatedData() { - glideComposeRule.setContent { - // Swap both to avoid confusing the preloader. The preloader doesn't notice or take into - // account data set changes (this is a bug in the Java preloading API)... - val currentPreloadModels = remember { mutableStateListOf() } - val currentModels = remember { mutableStateListOf() } - // Use a button to swap data because we can't mutate state in setContent easily from - // outside - // the method, nor can you call setContent multiple times. - fun swapData() { - currentPreloadModels.addAll(preloadModels) - currentModels.addAll(preloadModels) - } - val preloadData = - rememberGlidePreloadingData( - data = currentPreloadModels, - preloadImageSize = Target.SIZE_ORIGINAL.toSize(), - numberOfItemsToPreload = 1, - fixedVisibleItemCount = 1, - ) { data: Int, requestBuilder: RequestBuilder -> - requestBuilder.load(data).removeTheme() - } - - TextButton(onClick = ::swapData) { Text(text = "Swap") } - - Column { - LazyRow( - modifier = Modifier.testTag(listTestTag), - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - items(currentModels.size) { index -> - // This mismatch between currentModels and preloadData may lead to errors in - // the future - // because items may be recomposed before the setContent method's function - // is - // recomposed. See https://chat.google.com/room/AAAAYRnp4-Y/AvFrBgb_peU for - // a bunch of - // detailed discussion. - preloadData.triggerPreload(index) - GlideImage( - model = currentModels[index], - contentDescription = imageContentDescription(index), - Modifier.fillParentMaxWidth(), - ) - } - } - } + // Scroll to the 0th image, accounting for the first header item. + val scrollToIndex = 1 + glideComposeRule.onNode(hasTestTag(listTestTag)).performScrollToIndex(scrollToIndex) + // Make sure the next image, the 1th, is in memory due to preloading. + assertThatModelIsInMemoryCache(preloadModels[1]) + } + + @Test + fun glideLazyListPreloader_whenDataChanges_onScroll_preloadsUpdatedData() { + glideComposeRule.setContent { + // Swap both to avoid confusing the preloader. The preloader doesn't notice or take into + // account data set changes (this is a bug in the Java preloading API)... + val currentPreloadModels = remember { mutableStateListOf() } + val currentModels = remember { mutableStateListOf() } + // Use a button to swap data because we can't mutate state in setContent easily from + // outside + // the method, nor can you call setContent multiple times. + fun swapData() { + currentPreloadModels.addAll(preloadModels) + currentModels.addAll(preloadModels) + } + val preloadData = + rememberGlidePreloadingData( + data = currentPreloadModels, + preloadImageSize = Target.SIZE_ORIGINAL.toSize(), + numberOfItemsToPreload = 1, + fixedVisibleItemCount = 1, + ) { data: Int, requestBuilder: RequestBuilder -> + requestBuilder.load(data).removeTheme() } - glideComposeRule.onNodeWithText("Swap").performClick() - glideComposeRule.waitForIdle() - val scrollToIndex = 1 - glideComposeRule.onNode(hasTestTag(listTestTag)).performScrollToIndex(scrollToIndex) + TextButton(onClick = ::swapData) { Text(text = "Swap") } - assertThatModelIsInMemoryCache(preloadModels[scrollToIndex + 1]) + Column { + LazyRow( + modifier = Modifier.testTag(listTestTag), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + items(currentModels.size) { index -> + // This mismatch between currentModels and preloadData may lead to errors in + // the future + // because items may be recomposed before the setContent method's function + // is + // recomposed. See https://chat.google.com/room/AAAAYRnp4-Y/AvFrBgb_peU for + // a bunch of + // detailed discussion. + preloadData.triggerPreload(index) + GlideImage( + model = currentModels[index], + contentDescription = imageContentDescription(index), + Modifier.fillParentMaxWidth(), + ) + } + } + } } - @Test - fun glideLazyListPreloader_withHeaderItems_andPositionFunction_onScroll_preloadsTheFirstItem() { - val numHeaderItems = 3 - glideComposeRule.setContent { - val data = rememberOneItemAtATimePreloadingData() - LazyRow(modifier = Modifier.testTag(listTestTag)) { - repeat(numHeaderItems) { item { Text(text = "Header$it") } } - items(data.size) { index -> - data.triggerPreload(index) - GlideImage( - model = model, - contentDescription = imageContentDescription(index), - Modifier.fillParentMaxWidth(), - ) - } - } - } + glideComposeRule.onNodeWithText("Swap").performClick() + glideComposeRule.waitForIdle() + val scrollToIndex = 1 + glideComposeRule.onNode(hasTestTag(listTestTag)).performScrollToIndex(scrollToIndex) - val imageIndex = 1 - val scrollToIndex = numHeaderItems + imageIndex - glideComposeRule.onNode(hasTestTag(listTestTag)).performScrollToIndex(scrollToIndex) + assertThatModelIsInMemoryCache(preloadModels[scrollToIndex + 1]) + } - assertThatModelIsInMemoryCache(preloadModels[imageIndex + 1]) + @Test + fun glideLazyListPreloader_withHeaderItems_andPositionFunction_onScroll_preloadsTheFirstItem() { + val numHeaderItems = 3 + glideComposeRule.setContent { + val data = rememberOneItemAtATimePreloadingData() + LazyRow(modifier = Modifier.testTag(listTestTag)) { + repeat(numHeaderItems) { item { Text(text = "Header$it") } } + items(data.size) { index -> + data.triggerPreload(index) + GlideImage( + model = model, + contentDescription = imageContentDescription(index), + Modifier.fillParentMaxWidth(), + ) + } + } } - // Ignore the preload request because we want to test that the preloader loaded a model - // and not be confused by our UI loading a model. Do not ignore the preload request - // builder in real code! - @Composable - private fun GlidePreloadingData.triggerPreload(index: Int) = this[index].first + val imageIndex = 1 + val scrollToIndex = numHeaderItems + imageIndex + glideComposeRule.onNode(hasTestTag(listTestTag)).performScrollToIndex(scrollToIndex) + + assertThatModelIsInMemoryCache(preloadModels[imageIndex + 1]) + } - @Composable - private fun rememberOneItemAtATimePreloadingData(): GlidePreloadingData { - return rememberGlidePreloadingData( + @Test + fun rememberGlidePreloadingData_onDispose_cancelsAndClearsPreloads() { + var showPreloader by mutableStateOf(true) + val modelToPreload = preloadModels[1] // Preloader loads ahead, so triggering at 0 loads 1 + + glideComposeRule.setContent { + if (showPreloader) { + val preloadingData = + rememberGlidePreloadingData( data = preloadModels, preloadImageSize = Target.SIZE_ORIGINAL.toSize(), numberOfItemsToPreload = 1, fixedVisibleItemCount = 1, - ) { model, requestBuilder -> + ) { model, requestBuilder -> requestBuilder.load(model).removeTheme() - } + } + preloadingData.triggerPreload(0) + } } - private fun assertThatModelIsInMemoryCache(@DrawableRes model: Int) { - // Wait for previous async image loads to finish - glideComposeRule.waitForIdle() - val nextPreloadModel: Drawable = - Glide.with(context).load(model).removeTheme().onlyRetrieveFromCache(true).submit().get() - assertThat(nextPreloadModel).isNotNull() - } + // Verify it loaded into memory + assertThatModelIsInMemoryCache(modelToPreload) - // We're loading the same resource across two different Contexts. One is the Context from the - // instrumentation package, the other is the package under test. Each Context has it's own - // Theme, - // neither of which are equal to each other. So that we can verify an item is loaded into - // memory, - // we remove the themes from all requests that we need to have matching cache keys. - private fun RequestBuilder.removeTheme() = theme(null) - - private companion object { - const val model = android.R.drawable.star_big_on - - // Use different preload and non-preload models so that we can assert on which items are - // preloaded and not loaded by the list. This is bad practice in production code and would - // waste - // resources while doing nothing useful in a real app. - val preloadModels = - listOf( - android.R.drawable.btn_minus, - android.R.drawable.btn_radio, - android.R.drawable.btn_star, - ) + // Dispose the preloader + showPreloader = false + glideComposeRule.waitForIdle() + + // Clear memory cache. Active resources (leaked) will NOT be cleared. + // Inactive resources (cleared) WILL be cleared. + glideComposeRule.runOnUiThread { Glide.get(context).clearMemory() } + glideComposeRule.waitForIdle() + + // Verify if it's still in memory. + // If it leaked, it's still active, so this will succeed. + // If it was fixed, it was cleared, so this will fail. + val isStillInMemory = + try { + val future = + Glide.with(context) + .load(modelToPreload) + .removeTheme() + .diskCacheStrategy(DiskCacheStrategy.NONE) + .onlyRetrieveFromCache(true) + .submit() + future.get() + Glide.with(context).clear(future) + true + } catch (e: Exception) { + false + } + + assertThat(isStillInMemory).isFalse() + } - const val listTestTag = "listTestTag" + // Ignore the preload request because we want to test that the preloader loaded a model + // and not be confused by our UI loading a model. Do not ignore the preload request + // builder in real code! + @Composable + private fun GlidePreloadingData.triggerPreload(index: Int) = this[index].first - fun imageContentDescription(index: Int) = "Image $index" + @Composable + private fun rememberOneItemAtATimePreloadingData(): GlidePreloadingData { + return rememberGlidePreloadingData( + data = preloadModels, + preloadImageSize = Target.SIZE_ORIGINAL.toSize(), + numberOfItemsToPreload = 1, + fixedVisibleItemCount = 1, + ) { model, requestBuilder -> + requestBuilder.load(model).removeTheme() } + } + + private fun assertThatModelIsInMemoryCache(@DrawableRes model: Int) { + // Wait for previous async image loads to finish + glideComposeRule.waitForIdle() + val future = + Glide.with(context) + .load(model) + .removeTheme() + .diskCacheStrategy(DiskCacheStrategy.NONE) + .onlyRetrieveFromCache(true) + .submit() + val nextPreloadModel: Drawable = future.get() + assertThat(nextPreloadModel).isNotNull() + // Clear the future target to release the resource from "active" to "cached" state, + // otherwise it remains pinned in memory by this verification request. + Glide.with(context).clear(future) + } + + // We're loading the same resource across two different Contexts. One is the Context from the + // instrumentation package, the other is the package under test. Each Context has it's own + // Theme, + // neither of which are equal to each other. So that we can verify an item is loaded into + // memory, + // we remove the themes from all requests that we need to have matching cache keys. + private fun RequestBuilder.removeTheme() = theme(null) + + private companion object { + const val model = android.R.drawable.star_big_on + + // Use different preload and non-preload models so that we can assert on which items are + // preloaded and not loaded by the list. This is bad practice in production code and would + // waste + // resources while doing nothing useful in a real app. + val preloadModels = + listOf( + android.R.drawable.btn_minus, + android.R.drawable.btn_radio, + android.R.drawable.btn_star, + ) + + const val listTestTag = "listTestTag" + + fun imageContentDescription(index: Int) = "Image $index" + } } private fun Int.toSize() = this.toFloat().let { Size(it, it) } diff --git a/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Preload.kt b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Preload.kt index 378a4c4ad7..8e5faf3326 100644 --- a/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Preload.kt +++ b/integration/compose/src/main/java/com/bumptech/glide/integration/compose/Preload.kt @@ -2,6 +2,7 @@ package com.bumptech.glide.integration.compose import android.graphics.drawable.Drawable import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.geometry.Size @@ -46,7 +47,6 @@ private const val DEFAULT_ITEMS_TO_PRELOAD = 10 * rows prior to your first image, you can optionally add a few manual calls to make preloading * continue smoothly across data sets. One way you might do so is to call the next data set toward * the end of the previous data set, e.g.: - * * ``` * val itemsToPreload = 15 * items(firstDataSet.size) { index -> @@ -81,42 +81,47 @@ private const val DEFAULT_ITEMS_TO_PRELOAD = 10 */ @Composable public fun rememberGlidePreloadingData( - dataSize: Int, - dataGetter: (Int) -> DataT, - preloadImageSize: Size, - numberOfItemsToPreload: Int = DEFAULT_ITEMS_TO_PRELOAD, - fixedVisibleItemCount: Int? = null, - requestBuilderTransform: PreloadRequestBuilderTransform, + dataSize: Int, + dataGetter: (Int) -> DataT, + preloadImageSize: Size, + numberOfItemsToPreload: Int = DEFAULT_ITEMS_TO_PRELOAD, + fixedVisibleItemCount: Int? = null, + requestBuilderTransform: PreloadRequestBuilderTransform, ): GlidePreloadingData { - val requestManager = LocalContext.current.let { remember(it) { Glide.with(it) } } - return remember( - requestManager, + val requestManager = LocalContext.current.let { remember(it) { Glide.with(it) } } + val preloadData = + remember( + requestManager, + dataSize, + dataGetter, + preloadImageSize, + numberOfItemsToPreload, + fixedVisibleItemCount, + requestBuilderTransform, + ) { + val preloaderData = + PreloaderData(dataSize, dataGetter, requestBuilderTransform, preloadImageSize) + val preloader = + ListPreloader( + requestManager, + PreloadModelProvider(requestManager, preloaderData), + PreloadDimensionsProvider(preloaderData), + numberOfItemsToPreload, + ) + PreloadDataImpl( dataSize, dataGetter, + requestManager, preloadImageSize, - numberOfItemsToPreload, fixedVisibleItemCount, + preloader, requestBuilderTransform, - ) { - val preloaderData = - PreloaderData(dataSize, dataGetter, requestBuilderTransform, preloadImageSize) - val preloader = - ListPreloader( - requestManager, - PreloadModelProvider(requestManager, preloaderData), - PreloadDimensionsProvider(preloaderData), - numberOfItemsToPreload, - ) - PreloadDataImpl( - dataSize, - dataGetter, - requestManager, - preloadImageSize, - fixedVisibleItemCount, - preloader, - requestBuilderTransform, - ) + ) } + + DisposableEffect(preloadData) { onDispose { preloadData.clear() } } + + return preloadData } /** @@ -125,31 +130,31 @@ public fun rememberGlidePreloadingData( */ @Composable public fun rememberGlidePreloadingData( - data: List, - preloadImageSize: Size, - numberOfItemsToPreload: Int = DEFAULT_ITEMS_TO_PRELOAD, - fixedVisibleItemCount: Int? = null, - requestBuilderTransform: PreloadRequestBuilderTransform, + data: List, + preloadImageSize: Size, + numberOfItemsToPreload: Int = DEFAULT_ITEMS_TO_PRELOAD, + fixedVisibleItemCount: Int? = null, + requestBuilderTransform: PreloadRequestBuilderTransform, ): GlidePreloadingData { - return rememberGlidePreloadingData( - dataSize = data.size, - dataGetter = data::get, - preloadImageSize = preloadImageSize, - numberOfItemsToPreload = numberOfItemsToPreload, - fixedVisibleItemCount = fixedVisibleItemCount, - requestBuilderTransform = requestBuilderTransform, - ) + return rememberGlidePreloadingData( + dataSize = data.size, + dataGetter = data::get, + preloadImageSize = preloadImageSize, + numberOfItemsToPreload = numberOfItemsToPreload, + fixedVisibleItemCount = fixedVisibleItemCount, + requestBuilderTransform = requestBuilderTransform, + ) } private data class PreloaderData( - val dataSize: Int, - val dataAccessor: (Int) -> DataT, - val requestBuilderTransform: PreloadRequestBuilderTransform, - val size: Size, + val dataSize: Int, + val dataAccessor: (Int) -> DataT, + val requestBuilderTransform: PreloadRequestBuilderTransform, + val size: Size, ) { - fun preloadRequests(requestManager: RequestManager, item: DataT): RequestBuilder { - return requestBuilderTransform(item, requestManager.asDrawable()) - } + fun preloadRequests(requestManager: RequestManager, item: DataT): RequestBuilder { + return requestBuilderTransform(item, requestManager.asDrawable()) + } } /** @@ -157,76 +162,80 @@ private data class PreloaderData( * the data and the preload [RequestBuilder]. */ public interface GlidePreloadingData { - /** The total number of items in the data set. */ - public val size: Int - - /** - * Returns the [DataT] at a given index in the data and a [RequestBuilder] that will trigger a - * request that exactly matches the preload request for this index. - * - * The returned [RequestBuilder] should always be used to display the item at the given index. - * Otherwise the preload request triggered by this call is likely useless work. The - * [RequestBuilder] can either be used as the primary request, or more likely, passed as the - * [RequestBuilder.thumbnail] to a higher resolution request. - * - * This method has side affects! Calling it will trigger preloads based on the given [index]. - * Preloading assumes sequential access in a manner that matches what the user will see. If you - * need to look up data at indices for other reasons, use the underlying data source directly so - * that you do not confuse the preloader. Only use this method when obtaining data to display to - * the user. - */ - @Composable public operator fun get(index: Int): Pair> + /** The total number of items in the data set. */ + public val size: Int + + /** + * Returns the [DataT] at a given index in the data and a [RequestBuilder] that will trigger a + * request that exactly matches the preload request for this index. + * + * The returned [RequestBuilder] should always be used to display the item at the given index. + * Otherwise the preload request triggered by this call is likely useless work. The + * [RequestBuilder] can either be used as the primary request, or more likely, passed as the + * [RequestBuilder.thumbnail] to a higher resolution request. + * + * This method has side affects! Calling it will trigger preloads based on the given [index]. + * Preloading assumes sequential access in a manner that matches what the user will see. If you + * need to look up data at indices for other reasons, use the underlying data source directly so + * that you do not confuse the preloader. Only use this method when obtaining data to display to + * the user. + */ + @Composable public operator fun get(index: Int): Pair> } private class PreloadDataImpl( - override val size: Int, - private val indexToData: (Int) -> DataT, - private val requestManager: RequestManager, - private val preloadImageSize: Size, - private val fixedVisibleItemCount: Int?, - private val preloader: ListPreloader, - private val requestBuilderTransform: PreloadRequestBuilderTransform, + override val size: Int, + private val indexToData: (Int) -> DataT, + private val requestManager: RequestManager, + private val preloadImageSize: Size, + private val fixedVisibleItemCount: Int?, + private val preloader: ListPreloader, + private val requestBuilderTransform: PreloadRequestBuilderTransform, ) : GlidePreloadingData { - @Composable - override fun get(index: Int): Pair> { - val item = indexToData(index) - val requestBuilder = - requestBuilderTransform( - item, - requestManager - .asDrawable() - .override(preloadImageSize.width.toInt(), preloadImageSize.height.toInt()), - ) - - LaunchedEffect(preloader, preloadImageSize, requestBuilderTransform, indexToData, index) { - preloader.onScroll(/* absListView= */ null, index, fixedVisibleItemCount ?: 1, size) - } - return item to requestBuilder + @Composable + override fun get(index: Int): Pair> { + val item = indexToData(index) + val requestBuilder = + requestBuilderTransform( + item, + requestManager + .asDrawable() + .override(preloadImageSize.width.toInt(), preloadImageSize.height.toInt()), + ) + + LaunchedEffect(preloader, preloadImageSize, requestBuilderTransform, indexToData, index) { + preloader.onScroll(/* absListView= */ null, index, fixedVisibleItemCount ?: 1, size) } + return item to requestBuilder + } + + fun clear() { + preloader.cancelAll() + } } private class PreloadDimensionsProvider( - private val updatedData: PreloaderData + private val updatedData: PreloaderData ) : ListPreloader.PreloadSizeProvider { - override fun getPreloadSize(item: DataT, adapterPosition: Int, perItemPosition: Int): IntArray = - updatedData.size.toIntArray() + override fun getPreloadSize(item: DataT, adapterPosition: Int, perItemPosition: Int): IntArray = + updatedData.size.toIntArray() } private fun Size.toIntArray() = intArrayOf(width.toInt(), height.toInt()) private class PreloadModelProvider( - private val requestManager: RequestManager, - private val data: PreloaderData, + private val requestManager: RequestManager, + private val data: PreloaderData, ) : ListPreloader.PreloadModelProvider { - override fun getPreloadItems(position: Int): MutableList { - return mutableListOf(data.dataAccessor(position)) - } + override fun getPreloadItems(position: Int): MutableList { + return mutableListOf(data.dataAccessor(position)) + } - override fun getPreloadRequestBuilder(item: DataT): RequestBuilder<*> { - return data.preloadRequests(requestManager, item) - } + override fun getPreloadRequestBuilder(item: DataT): RequestBuilder<*> { + return data.preloadRequests(requestManager, item) + } } /** @@ -237,4 +246,4 @@ private class PreloadModelProvider( * `requestBuilder` to customize your load. */ public typealias PreloadRequestBuilderTransform = - (item: DataTypeT, requestBuilder: RequestBuilder) -> RequestBuilder + (item: DataTypeT, requestBuilder: RequestBuilder) -> RequestBuilder diff --git a/library/src/main/java/com/bumptech/glide/ListPreloader.java b/library/src/main/java/com/bumptech/glide/ListPreloader.java index 9946fd0337..5d10d0d574 100644 --- a/library/src/main/java/com/bumptech/glide/ListPreloader.java +++ b/library/src/main/java/com/bumptech/glide/ListPreloader.java @@ -227,7 +227,7 @@ private void preloadItem(@Nullable T item, int position, int perItemPosition) { preloadRequestBuilder.into(preloadTargetQueue.next(dimensions[0], dimensions[1])); } - private void cancelAll() { + public void cancelAll() { for (int i = 0; i < preloadTargetQueue.queue.size(); i++) { requestManager.clear(preloadTargetQueue.next(0, 0)); }