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.
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'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()
}
}
I'm new to Jetpack Compose and I'm not quite sure how to do what I need. In the screen below, I want to scroll the whole screen and not just the list at the bottom and when the scroll reaches the end of the list below, it still applies the paging library and goes to get more elements. I managed to get the Paging Library to work and the scroll in the list below too, but I can't make the rest of the page elements scroll as well - this is because only the list has scroll and not the rest of the page. Whenever I'm trying to do that, I get the following crash:
Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()). If you want to add a header before the list of items please add a header as a separate item() before the main items() inside the LazyColumn scope. There are could be other reasons for this to happen: your ComposeView was added into a LinearLayout with some weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a custom layout. Please try to remove the source of infinite constraints in the hierarchy above the scrolling container.
and I don't really know why.
I leave you the code below and two screenshots: the first is the current state, where I can only scroll through the list. The second is what I intend, which is to scroll the entire page.
#Edit: I was able to implement all screen scroll with fixed height on the children lazy column, but that is not what I want.
#Composable
#ExperimentalFoundationApi
private fun MainActivityLayout(navController: NavHostController) {
LazyColumn(
modifier = Modifier
.paint(
painter = painterResource(id = R.drawable.main_background),
contentScale = ContentScale.FillBounds
)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
item {
HeightSpacer(Dimen40)
Image(
painter = painterResource(id = R.drawable.ic_clearjobs_logo_2x),
contentDescription = null
)
HeightSpacer(Dimen47)
Navigation(navController = navController)
}
}
}
#Composable
#ExperimentalFoundationApi
fun JobOpeningsScreen(viewModel: JobOpeningsViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
Column {
ClearJobsScreenTitle(
lightTitle = stringResource(id = R.string.job_openings_light_title),
boldTitle = stringResource(id = R.string.job_openings_bold_title)
)
HeightSpacer(Dimen60)
Row {
CategoryButton()
WidthSpacer(Dimen2)
OrderByButton()
}
HeightSpacer(Dimen30)
SearchTextField()
HeightSpacer(Dimen60)
when (uiState) {
is BaseViewState.Data -> JobOpeningsContent(
viewState = uiState.cast<BaseViewState.Data<JobOpeningsViewState>>().value
)
is BaseViewState.Loading -> {
LoadingView()
}
else -> {}
}
LaunchedEffect(key1 = viewModel, block = {
viewModel.onTriggerEvent(JobOpeningsEvent.LoadJobOffers)
})
}
}
#Composable
fun JobOpeningsContent(viewState: JobOpeningsViewState) {
val pagingItems = rememberFlowWithLifecycle(viewState.pagedData).collectAsLazyPagingItems()
SwipeRefresh(
state = rememberSwipeRefreshState(
isRefreshing = pagingItems.loadState.refresh == LoadState.Loading
),
onRefresh = { pagingItems.refresh() },
indicator = { state, trigger ->
SwipeRefreshIndicator(
state = state,
refreshTriggerDistance = trigger,
scale = true
)
},
content = {
LazyColumn(
modifier = Modifier.width(Dimen320),
verticalArrangement = Arrangement.spacedBy(Dimen30)
) {
items(pagingItems.itemCount) { index ->
pagingItems[index]?.let {
JobOpeningsRow(dto = it)
}
}
if (pagingItems.loadState.append == LoadState.Loading) {
item {
Box(
Modifier
.padding(24.dp)
) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
}
}
)
}
I found the solution to this problem, although it is not 100% and in terms of code it is not as good as I would like.
The error speaks for itself, we can't have infinite vertical scroll, Jetpack Compose doesn't allow it. I had the option of putting a fixed height on the Lazy Column of my list, but it wasn't what I wanted and it didn't work properly. The solution was to put everything inside a single LazyColumn and remove the Column from MainActivity, using a Box element and contentAlignment. I leave you below the final code that I used to solve the problem.
MainScreen function that before was MainActivityLayout function:
#Preview
#Composable
#ExperimentalFoundationApi
fun MainScreen() {
val navController = rememberNavController()
val topLevelDestinations = listOf(
NavigationItem.JobOpenings,
NavigationItem.Profile,
NavigationItem.About
)
val isTopLevelDestination =
navController
.currentBackStackEntryAsState()
.value
?.destination
?.route in topLevelDestinations.map { it.route }
val backStackEntryState = navController.currentBackStackEntryAsState()
Scaffold(
bottomBar = {
if (isTopLevelDestination) {
BottomNavBar(
navController = navController,
backStackEntryState = backStackEntryState,
bottomNavItems = topLevelDestinations
)
}
}
) {
Box(
modifier = Modifier
.paint(
painter = painterResource(id = R.drawable.main_background),
contentScale = ContentScale.FillBounds
)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Navigation(navController = navController)
}
}
}
New JobOpenings fun that is mixed with old JobOpeningsContent function:
#Composable
#ExperimentalFoundationApi
fun JobOpeningsScreen(viewModel: JobOpeningsViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
is BaseViewState.Data -> {
val pagedData = uiState.cast<BaseViewState.Data<JobOpeningsViewState>>().value.pagedData
val pagingItems = rememberFlowWithLifecycle(pagedData).collectAsLazyPagingItems()
SwipeRefresh(
state = rememberSwipeRefreshState(
isRefreshing = pagingItems.loadState.refresh == LoadState.Loading
),
onRefresh = { pagingItems.refresh() },
indicator = { state, trigger ->
SwipeRefreshIndicator(
state = state,
refreshTriggerDistance = trigger,
scale = true
)
},
content = {
LazyColumn(
modifier = Modifier
.width(Dimen320),
verticalArrangement = Arrangement.spacedBy(Dimen30)
) {
item {
ScreenHeader(
lightTitle = stringResource(id = R.string.job_openings_light_title),
boldTitle = stringResource(id = R.string.job_openings_bold_title)
)
HeightSpacer(Dimen60)
Row {
CategoryButton()
WidthSpacer(Dimen2)
OrderByButton()
}
HeightSpacer(Dimen30)
SearchTextField()
HeightSpacer(Dimen60)
}
items(pagingItems.itemCount) { index ->
pagingItems[index]?.let {
JobOpeningsRow(dto = it)
}
}
if (pagingItems.loadState.append == LoadState.Loading) {
item {
Box(Modifier.padding(Dimen24)) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
}
}
)
}
is BaseViewState.Loading -> LoadingView()
else -> {}
}
LaunchedEffect(key1 = viewModel, block = {
viewModel.onTriggerEvent(JobOpeningsEvent.LoadJobOffers)
})
}
#ExperimentalFoundationApi
#Preview
#Composable
fun JobOpenings() {
JobOpeningsScreen()
}
Problems that I found with this solution:
LoadingView appears at the top of the screen instead at the top of the list.
If anyone has any suggestion to improve this, I am open to it. This works perfectly with Paging Library + Swipe Refresh (Accompanist) and full page scroll.
I'm new to Jetpack Compose. I am currently developing a chat application. I ask the user to choose an image from the gallery or take a picture from the camera. Then I save the file Uri to the database and then listen to the list of all messages. When this list is updated, this image is recomposing and it flashes.
Messages list in viewmodel:
private var _messages = MutableStateFlow<List<ChatUiMessage>>(mutableListOf())
val messages: StateFlow<List<ChatUiMessage>> = _messages
...
private fun observeMessages() {
viewModelScope.launch {
chatManager.observeMessagesFlow()
.flowOn(dispatcherIO)
.collect {
_messages.emit(it)
}
}
}
Main chat screen:
...
val messages by viewModel.messages.collectAsState(listOf())
...
val listState = rememberLazyListState()
LazyColumn(
modifier = modifier.fillMaxWidth(),
reverseLayout = true,
state = listState
) {
itemsIndexed(items = messages) { index, message ->
when (message) {
...
is ChatUiMessage.Image -> SentImageBlock(
message = message
)
...
}
}
}
My SentImageBlock:
#Composable
private fun SentImageBlock(message: ChatUiMessage.Image) {
val context = LocalContext.current
val bitmap: MutableState<Bitmap?> = rememberSaveable { mutableStateOf(null) }
bitmap.value ?: run {
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
bitmap.value = try {
when {
Build.VERSION.SDK_INT >= 28 -> {
val source = ImageDecoder.createSource(context.contentResolver, message.fileUriPath.toUri())
ImageDecoder.decodeBitmap(source)
}
else -> {
MediaStore.Images.Media.getBitmap(context.contentResolver, message.fileUriPath.toUri())
}
}
} catch (e: Exception) {
null
}
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.padding(end = 16.dp, top = 16.dp, bottom = 16.dp)
.heightIn(max = 200.dp, min = 200.dp)
) {
bitmap.value?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterEnd)
)
}
}
StandardText(text = message.sendFileStatus.toString())
StandardText(text = message.fileType.toString())
}
I have tried several things and either is the Image always blinking.
LazyColumn reuses views for items using key argument, and by default it's equal to item index. You can provide a correct key(something like message id) for views to be reused correctly:
val messages = listOf(1,2,3)
LazyColumn(
modifier = modifier.fillMaxWidth(),
reverseLayout = true,
state = listState
) {
itemsIndexed(
items = messages,
key = { index, message -> message.id }
) { index, message ->
when (message) {
...
is ChatUiMessage.Image -> SentImageBlock(
message = message
)
...
}
}
}
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) }