I'm using below code to retrieve video frame from remote url using MediaMetadataRetriever but it lags and results in very low performant UI. How can I make it fast and efficient?
#Composable
private fun ContentItem(
modifier: Modifier = Modifier,
content: Content,
onClick: (Content) -> Unit
) {
when (content.type) {
ContentType.Image -> {
// handle image
}
ContentType.Video -> {
val bitmap = remember { mutableStateOf<Bitmap?>(null) }
LaunchedEffect(content) {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(content.url)
// Retrieve frame at 1 second
bitmap.value = retriever.getFrameAtTime(
1000000,
MediaMetadataRetriever.OPTION_CLOSEST_SYNC
)
retriever.release()
}
bitmap.value?.let {
Image(
modifier = modifier,
bitmap = it.asImageBitmap(),
contentDescription = null
)
}
}
}
}
Instead of doing heavy work on ui thread you can use withContext and Dispatchers.Default as
LaunchedEffect(content) {
withContext(Dispatchers.Default){
val retriever = MediaMetadataRetriever()
retriever.setDataSource(content.url)
// Retrieve frame at 1 second
bitmap.value = retriever.getFrameAtTime(
1000000,
MediaMetadataRetriever.OPTION_CLOSEST_SYNC
)
retriever.release()
}
}
Related
I tried to implement pagination in my app using compose. Here you can find full code : https://github.com/alirezaeiii/TMDb-Compose
First I am showing a loading indicator, and then loads items from TMDb API. So I have following composable method :
#Composable
fun <T : TMDbItem> PagingScreen(
viewModel: BasePagingViewModel<T>,
onClick: (TMDbItem) -> Unit,
) {
val lazyTMDbItems = viewModel.pagingDataFlow.collectAsLazyPagingItems()
when (lazyTMDbItems.loadState.refresh) {
is LoadState.Loading -> {
TMDbProgressBar()
}
is LoadState.Error -> {
val message =
(lazyTMDbItems.loadState.refresh as? LoadState.Error)?.error?.message ?: return
lazyTMDbItems.apply {
ErrorScreen(
message = message,
modifier = Modifier.fillMaxSize(),
refresh = { retry() }
)
}
}
else -> {
LazyTMDbItemGrid(lazyTMDbItems, onClick)
}
}
}
As you see in the else section it calls LazyTMDbItemGrid composable function :
#Composable
private fun <T : TMDbItem> LazyTMDbItemGrid(
lazyTMDbItems: LazyPagingItems<T>,
onClick: (TMDbItem) -> Unit
) {
LazyVerticalGrid(
columns = GridCells.Fixed(COLUMN_COUNT),
contentPadding = PaddingValues(
start = Dimens.GridSpacing,
end = Dimens.GridSpacing,
bottom = WindowInsets.navigationBars.getBottom(LocalDensity.current)
.toDp().dp.plus(
Dimens.GridSpacing
)
),
horizontalArrangement = Arrangement.spacedBy(
Dimens.GridSpacing,
Alignment.CenterHorizontally
),
content = {
items(lazyTMDbItems.itemCount) { index ->
val tmdbItem = lazyTMDbItems[index]
tmdbItem?.let {
TMDbItemContent(
it,
Modifier
.height(320.dp)
.padding(vertical = Dimens.GridSpacing),
onClick
)
}
}
lazyTMDbItems.apply {
when (loadState.append) {
is LoadState.Loading -> {
item(span = span) {
LoadingRow(modifier = Modifier.padding(vertical = Dimens.GridSpacing))
}
}
is LoadState.Error -> {
val message =
(loadState.append as? LoadState.Error)?.error?.message ?: return#apply
item(span = span) {
ErrorScreen(
message = message,
modifier = Modifier.padding(vertical = Dimens.GridSpacing),
refresh = { retry() })
}
}
else -> {}
}
}
})
}
In order to load images in the ImageView asynchronously, I have following function :
#Composable
private fun BoxScope.TMDbItemPoster(posterUrl: String?, tmdbItemName: String) {
val painter = rememberAsyncImagePainter(
model = posterUrl,
error = rememberVectorPainter(Icons.Filled.BrokenImage),
placeholder = rememberVectorPainter(Icons.Default.Movie)
)
val colorFilter = when (painter.state) {
is AsyncImagePainter.State.Loading, is AsyncImagePainter.State.Error -> ColorFilter.tint(
MaterialTheme.colors.imageTint
)
else -> null
}
val scale =
if (painter.state !is AsyncImagePainter.State.Success) ContentScale.Fit else ContentScale.FillBounds
Image(
painter = painter,
colorFilter = colorFilter,
contentDescription = tmdbItemName,
contentScale = scale,
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center)
)
}
The problem is when I am scrolling very fast in the part of screen that images are not loaded yet, it is quite laggy. Based on my research there is no problem with my image loading using coil library, but I do not know why it is laggy. Do you have any suggestion about this?
Addenda :
I have reported the issue here : https://issuetracker.google.com/issues/264847068
You can try to refer to this issue address on github to solve it. At present, Coil is combined with Compose's LazyVerticalGrid and LazyColumn, and the sliding sense is lagging:
https://github.com/coil-kt/coil/issues/1337
I have a screen which shows LazyVerticalGrid with Pictures and TopBar with OnClick that creates new activity for result to choose a Directory for pictures to show
#ExperimentalFoundationApi
#Composable
fun DisplayPictures(
pictures: List<Uri>,
navController: NavController,
appBarName: String = stringResource(id = R.string.choose_folder),
onNewDirectoryUri: (uri: Uri?) -> Unit = { }
) {
var showDirectorySelect by remember { mutableStateOf(false) }
if (showDirectorySelect) {
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocumentTree(),
onResult = {
onNewDirectoryUri(it)
showDirectorySelect = false
})
LaunchedEffect(key1 = Unit) {
launcher.launch(null)
}
}
Scaffold(topBar = {
TopAppBar(title = {
Text(
text = appBarName.dropLastWhile { predicate -> predicate == '%' },
Modifier.padding(8.dp)
)
}, Modifier.clickable { showDirectorySelect = true })
})
{
if (!showDirectorySelect) {
LazyVerticalGrid(
maxColumnWidth = 150.dp,
paddingDp = 4.dp,
pictures = pictures
) { uri ->
val route = Screen.Detail.createRoute(
URLEncoder.encode(
uri.toString(),
"UTF-8"
)
)
//Prevent multi-clicking and multi-touch
if (!navController.currentDestination?.route?.contains("detail")!!) {
navController.navigate(route)
}
}
}
}
}
But for some reason my app crashes with TransactionTooLargeException after user chose directory or just closed the app to background(clicked home button for example). I guess the problem is in LazyVerticalGrid(Its another composable function which is just lazy column with rows) which contains Pictures. So Picture is Composable function which loads Image from URI
//A wrapper around Image that shows placeholder and loading image via URI
#ExperimentalFoundationApi
#Composable
fun Picture(
uri: Uri,
modifier: Modifier,
size: Size,
onClick: () -> Unit = {},
) {
val bitmap: MutableState<Bitmap?> = rememberSaveable { mutableStateOf(null) }
//Coil and other libraries that can get image from uri get context this way
val context = LocalContext.current
bitmap.value ?: run {
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
try {
bitmap.value = context.contentResolver.loadThumbnail(uri, size, null)
} catch (e: Exception) {
}
}
}
}
Box(
modifier = modifier
.aspectRatio(1f)
.placeholder(visible = bitmap.value == null)
) {
bitmap.value?.let {
Log.i("longgg",uri.toString())
Image(
bitmap = it.asImageBitmap(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(RectangleShape)
.fillMaxSize()
.clickable { onClick() }
)
}
}
As I researched the whole Compose function DisplayPictures does recompose and LazyGrid download all pictures again after ActivityResultContract. This results to load a lot of thumbnails and I get a TransactionTooLargeException. The onNewDirectoryUri creates new mvi Event which starts DisplayPictures but with new params(new pictures and etc). How can I fix this behavior?
I found out that loadThumbnail method creates bundle under the hood
final Bundle opts = new Bundle();
opts.putParcelable(EXTRA_SIZE, new Point(size.getWidth(), size.getHeight()));
TransactionTooLargeException occurs when the data being passed among two activities is too large I think the bundle has limitations on how much data it can carry. Check what result you are sending through bundles.
Try to avoid sending bitmaps instead just send the URL of the image and reload the same in your activity.
I have VideoListScreen with LazyColumn and as my item I use VideoItem. This LazyColumn it's created with grid items to have lazy grid view with Category header. Tag is tag of category. Category details is information about category colors, title etc.:
#Composable
fun VideoItem(
videoPath: String,
brush: Brush,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
val assetFileDescriptor = LocalContext.current.assets.open(videoPath)
Surface(
modifier = modifier
.padding(5.dp)
.aspectRatio(1f)
.clickable { onClick() },
shape = Shapes.small,
elevation = 1.dp
) {
GlideImage(
imageModel = assetFileDescriptor.readBytes(),
contentScale = ContentScale.Crop,
requestOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.NONE),
shimmerParams = ShimmerParams(
baseColor = MaterialTheme.colors.background,
highlightColor = Blue200,
durationMillis = 650,
dropOff = 0.65f,
tilt = 20f
)
)
Box(modifier = Modifier
.background(brush)
.fillMaxSize() )
}
}
VideoListScreen:
#Composable
fun VideoListScreen(
navController: NavHostController,
tag: String
) {
val cells = 2
val context = LocalContext.current
val categoryDetails = getCategoryDetailsBy(tag)
val videos = fetchVideos(context, tag)
LazyColumn(contentPadding = PaddingValues(5.dp)) {
item {
CategoryElement(
categoryDetails = categoryDetails,
modifier = Modifier
.fillMaxWidth()
.height(130.dp)
.padding(5.dp),
customTitle = "O kategorii"
)
}
gridItems(videos, cells) { assetFileName ->
val videoPath = "$tag/$assetFileName"
VideoItem(
videoPath = videoPath,
brush = categoryDetails.transparentBrush
) { navController.navigateToPlayer(videoPath) } //onClick function
}
}
}
private fun fetchVideos(context: Context, tag: String): List<String> {
return context.resources.assets.list("$tag/")?.toList() ?: listOf()
}
gridItems extension function:
fun <T> LazyListScope.gridItems(
data: List<T>,
cells: Int,
itemContent: #Composable BoxScope.(T) -> Unit,
) {
items(data.chunked(cells)) { row ->
Row(Modifier.fillMaxWidth()) {
for ((index, item) in row.withIndex()) {
Box(Modifier.fillMaxWidth(1f / (cells - index))) {
itemContent.invoke(this, item)
}
}
}
}
}
The problem is that when I try to apply clickability on this item (no matter where) the thumbnail loading (from the assets) becomes almost twice as slow. What's interesting when onClick function is empty, performance issue disappearing. In function called "navigateToPlayer(videoPath)" I navigate to another screen and send "videoPath" with navController.
If you have any questions, feel free to ask!
In compose you're creating UI with view builders. This function can be called many times, when you start using animations it even can be recomposed on each frame.
That's why you shouldn't perform any heavy work directly in composable function. And if you do, you need to store the result so you don't need recalculation on the next recomposition.
Both fetchVideos and assets.open are quite heavy operations, and even result of getCategoryDetailsBy(not sure what's that) should be cached. To do that you need to use remember or rememberSaveable. Check out how are these different and more about state in composables.
So update your declarations like this:
val categoryDetails = remember { getCategoryDetailsBy(tag) }
val videos = remember { fetchVideos(context, tag) }
val context = LocalContext.current
val assetFileDescriptor = remember { context.assets.open(videoPath) }
I need to reload/refresh the coil image because it has been rewritten to disk/storage
The code I am using
val context = LocalContext.current
val loader = if (!isSvg) {
LocalImageLoader.current ?: remember { ImageLoader(context) }
} else {
remember {
ImageLoader.Builder(context)
.componentRegistry {
add(SvgDecoder(context))
}.build()
}
}
val painter = rememberCoilPainter(request = request, imageLoader = loader)
LaunchedEffect(key1 = invalidate, block = {
val result = loader.execute(
ImageRequest.Builder(context)
.diskCachePolicy(if (fromCache) CachePolicy.ENABLED else CachePolicy.DISABLED)
.data(request).build()
)
Log.d("TL_ImageLoadRes", result.toString())
})
Box(modifier = Modifier.background(MaterialTheme.colors.onBackground.copy(.4f))) {
Image(
modifier = modifier,
painter = painter,
contentDescription = stringResource(R.string.random_image),
contentScale = contentScale
)
}
Every time invalidate changes , it means the image has been rewritten and now may be found and I execute the LaunchedEffect but after getting the success result I don't know where to put it , it contains a Drawable , I don't know what to do with it
I am using accompanist-coil:0.12.0. I want to load image from a url and then pass the drawable to a method. I am using this:
val painter = rememberCoilPainter(
request = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.target {
viewModel.calcDominantColor(it) { color ->
dominantColor = color
}
}
.build(),
fadeIn = true
)
and then passing the painter to Image like this:
Image(
painter = painter,
contentDescription = "Some Image",
)
The image loads without any problem but the method calcDominantColor is never called.
Am I doing it the wrong way?
UPDATE:
I was able to call the method using Transformation in requestBuilder but I am not sure, if this is how it is supposed to be done because I am not actually transforming the Bitmap itself:
val painter = rememberCoilPainter(
request = entry.imageUrl,
requestBuilder = {
transformations(
object: Transformation{
override fun key(): String {
return entry.imageUrl
}
override suspend fun transform(
pool: BitmapPool,
input: Bitmap,
size: Size
): Bitmap {
viewModel.calcDominantColor(input) { color ->
dominantColor = color
}
return input
}
}
)
}
)
This works fine for first time but when the composable recomposes, transformation is returned from cache and my method doesn't run.
I think you want to use LaunchedEffect along with an ImageLoader to access the bitmap from the loader result.
val context = LocalContext.current
val imageLoader = ImageLoader(context)
val request = ImageRequest.Builder(context)
.transformations(RoundedCornersTransformation(12.dp.value))
.data(imageUrl)
.build()
val imagePainter = rememberCoilPainter(
request = request,
imageLoader = imageLoader
)
LaunchedEffect(key1 = imagePainter) {
launch {
val result = (imageLoader.execute(request) as SuccessResult).drawable
val bitmap = (result as BitmapDrawable).bitmap
val vibrant = Palette.from(bitmap)
.generate()
.getVibrantColor(defaultColor)
// do something with vibrant color
}
}
I would suggest using the new coil-compose library. Just copy the following and add it to the app build.gradle file:
implementation "io.coil-kt:coil-compose:1.4.0"
I was also following the tutorial and got stuck at this point. I would suggest copying and pasting the following code:
Column {
val painter = rememberImagePainter(
data = entry.imageUrl
)
val painterState = painter.state
Image(
painter = painter,
contentDescription = entry.pokemonName,
modifier = Modifier
.size(120.dp)
.align(CenterHorizontally),
)
if (painterState is ImagePainter.State.Loading) {
CircularProgressIndicator(
color = MaterialTheme.colors.primary,
modifier = Modifier
.scale(0.5f)
.align(CenterHorizontally)
)
}
else if (painterState is ImagePainter.State.Success) {
LaunchedEffect(key1 = painter) {
launch {
val image = painter.imageLoader.execute(painter.request).drawable
viewModel.calcDominantColor(image!!) {
dominantColor = it
}
}
}
}
Text(
text = entry.pokemonName,
fontFamily = RobotoCondensed,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
Replace your "AsyncImage" with "AsyncImageWithDrawable" :
#Composable
fun AsyncImageWithDrawable(
model: Any?,
contentDescription: String?,
modifier: Modifier = Modifier,
placeholderResId: Int? = null,
errorResId: Int? = null,
fallbackResId: Int? = errorResId,
contentScale: ContentScale,
onDrawableLoad: (Drawable?) -> Unit) {
val painter = rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = model)
.apply(block = fun ImageRequest.Builder.() {
crossfade(true)
placeholderResId?.let { placeholder(it) }
errorResId?.let { error(it) }
fallbackResId?.let { fallback(it) }
allowHardware(false)
}).build()
)
val state = painter.state
Image(
painter = painter,
contentDescription = contentDescription,
modifier = modifier,
contentScale = contentScale
)
when (state) {
is AsyncImagePainter.State.Success -> {
LaunchedEffect(key1 = painter) {
launch {
val drawable: Drawable? =
painter.imageLoader.execute(painter.request).drawable
onDrawableLoad(drawable)
}
}
}
else -> {}
}
}
I've created this Composable inspired by #Aknk answer and Coil source code.
Hint: You can use this Composable to Load and Render your Image from Url and get your Image Palette by the returned Drawable.
You can use the onSuccess callback to get the drawable:
AsyncImage(
model = url,
contentDescription = null,
onSuccess = { success ->
val drawable = success.result.drawable
}
)
You can use the same approach also with rememberAsyncImagePainter.