How to optimise laggy JetPack Compose layout - android

I'm using a HorizontalPager from accompanist package. The pager will have 1 page for each exercise stored by the user.
Each page in the pager has a LazyColumn which contain multiple cards with controls such as IconButtons and BasicTextField. The number of cards depends on number of sets configured by the user. I expect the typical number to be between 1 to 8 but only 3 to 5 would be visible on the screen at any given time (depending on the screen size and resolution).
The issue is that when this layout produces noticeable lag (animations skip frames) every time the HorizontalPager needs to build a new page that has more than 3 cards. This happens when swapping between pages. The same happens in debug and release versions running on a real device (Galaxy S10e) and emulator.
I'm trying to optimise this layout, so each frame renders in no more than 16ms regardless of the number of cards shown on the screen.
I've previously tried to solve this issue by setting fixed heights to some composables but that didn't help much. I've also tried using Text instead of BasicTextField, which would be then replaced with BasicTextField when users taps on the text but this hasn't helped much, therefore I removed this implementation.
Do you have some suggestions how performance of this layout could be improved to eliminate the lag?
Below is my code, screen shoot of the app screen and profiler:
#ExperimentalPagerApi
#Composable
fun WorkoutSessionScreen(
navHostController: NavHostController,
) {
val pagerState = rememberPagerState()
Scaffold(
topBar = { MyTopAppBar(navHostController = navHostController) }
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.background(MaterialTheme.colors.background)
.imePadding()
) {
HorizontalPager(
count = 10, state =
pagerState,
itemSpacing = 16.dp
) {
Log.e("==>", "Building horizontal pager")
TrackingControls()
}
Box(
modifier = Modifier
.height(70.dp)
.fillMaxWidth()
.background(Color(0xFAF7F7FF))
.align(Alignment.BottomCenter)
) {
Column(
Modifier.fillMaxWidth()
) {
Divider(color = Color(0x2A5C5C5C))
BottomControls()
}
}
}
}
}
#Composable
private fun TrackingControls() {
LazyColumn(
Modifier
.fillMaxHeight()
.fillMaxWidth(),
contentPadding = PaddingValues(vertical = 8.dp)
) {
items(6) { item ->
SetsAndRepsTrackingControls(
modifier = Modifier
)
}
}
}
#Composable
private fun BottomControls() {
Text(text = "Bottom Controlls")
}
#Composable
fun SetsAndRepsTrackingControls(modifier: Modifier = Modifier) {
val add = painterResource(id = R.drawable.ic_round_add_24)
val remove = painterResource(id = R.drawable.ic_round_remove_24)
Card(
modifier
.fillMaxWidth()
.height(200.dp)
.padding(vertical = 16.dp, horizontal = 8.dp),
backgroundColor = MaterialTheme.colors.surface,
shape = RoundedCornerShape(12.dp),
) {
Column() {
ControlsHeader()
TrackingInput(label = "REPS", add, remove)
Divider(color = Color.LightGray)
TrackingInput(label = "WEIGHT (KG)", add, remove)
}
}
}
#Composable
private fun ControlsHeader() {
Row(
modifier = Modifier
.height(56.dp)
.fillMaxWidth()
.background(MaterialTheme.colors.primary, RoundedCornerShape(12.dp))
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = "Set 1")
Text(text = "CheckBox")
}
}
#Composable
private fun TrackingInput(label: String = "Preview", add: Painter, remove: Painter) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = {}) {
Icon(
painter = painterResource(id = R.drawable.ic_round_remove_24),
contentDescription = "Minus",
tint = MaterialTheme.colors.onSurface
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
BasicTextField(
singleLine = true,
value = "8",
onValueChange = {},
textStyle = TextStyle(
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSurface
),
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = label,
style = MaterialTheme.typography.overline.copy(color = MaterialTheme.colors.onSurface)
)
}
IconButton(onClick = { Log.d("==>", "tada") }) {
Icon(
painter = painterResource(id = R.drawable.ic_round_add_24),
contentDescription = "Minus",
tint = MaterialTheme.colors.onSurface
)
}
}
}
This profiler print screen shows rendering of a single card called SetsAndRepsTrackingControls

Related

I have made horizontal pager with get data from API in jetpack compose, and i dont know how to make my pager scroll automaticly like a slider

Would you like to help me, i have no idea what i am doing. I want to make a dashboard and there are banner inside it. so i decide to make banner and get data from api to fill it in banner. but i want to make it scroll automaticly
Dashboard screen
#OptIn(ExperimentalPagerApi::class, FlowPreview::class)
#Composable
fun DashboardScreen(
navController: NavHostController = rememberAnimatedNavController(),
datastore: DataStoreRepository,
dashboardViewModel: DashboardViewModel = hiltViewModel(),
) {
val state by dashboardViewModel.dashboardState.collectAsState()
val username = datastore.getUsername().collectAsState(initial = "")
val company = datastore.getUserCompany().collectAsState(initial = "")
val pagerState = rememberPagerState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp, vertical = 20.dp)
.padding(12.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
TopSectionDashboard(name = username.value, company = company.value)
Spacer(modifier = Modifier.height(20.dp))
Box {
state.dashboard?.let {
BannerSlider(
state = pagerState,
listBanner = it.banner
)
Indicators(
size = it.banner.size,
index = pagerState.currentPage,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
if (state.isLoading) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator()
}
}
}
}
BannerSlider
#OptIn(ExperimentalPagerApi::class)
#Composable
fun BannerSlider(
modifier: Modifier = Modifier,
state: PagerState,
listBanner: List<Banner>,
) {
HorizontalPager(
count = listBanner.size,
state = state
) {
BannerItem(
item = Banner(
count = listBanner[it].count,
text = listBanner[it].text,
icon = listBanner[it].icon
),
)
}
}
Banner Item
#Composable
fun BannerItem(item: Banner, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.height(200.dp)
) {
Image(
painter = painterResource(id = R.drawable.background_dashboard_slider1),
contentDescription = null,
modifier = Modifier.fillMaxSize(),
alignment = Alignment.Center,
contentScale = ContentScale.Crop,
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.align(Alignment.CenterStart)
.offset(y = (-20).dp)
) {
Column(horizontalAlignment = Alignment.Start) {
Text(
text = item.text,
fontFamily = Poppins,
fontSize = 20.sp,
color = Color.White,
modifier = Modifier.padding(horizontal = 24.dp)
)
Text(
text = "${item.count}",
fontFamily = Poppins,
fontSize = 25.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier
.padding(horizontal = 24.dp)
.offset(y = (-10).dp)
)
}
AsyncImage(
model = item.icon,
contentDescription = "$item.title",
modifier = Modifier.size(120.dp),
alignment = Alignment.CenterEnd
)
}
}
}
Please help me, i have no idea. i really appreciate it to someone who give a hand to me
in a simple way, You can make it possible by
LaunchedEffect(pagerState.currentPage) {
launch {
delay(2000L)
with(pagerState) {
val nextPage = if (currentPage < pageCount - 1) currentPage + 1 else 0
animateScrollToPage(nextPage)
}
}
}
Keep it in my mind to pass count to HorizontalPager as well

Jetpack compose - Lazy Column crashes on Recompose

Hi I am using Jetpack Compose to create a Heterogenous list. I was successful in implementing it. My requirement is when I try to click an Item in the list, I need to recompose the list. The app crashes when tried to refresh the list with below exception:
java.lang.IllegalStateException: 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
My code is below:
#Composable fun PrepareOverViewScreen(overViewList: List<OverViewListItem>) {
Scaffold(topBar = { TopBar("OverView") },
content = { DisplayOverViewScreen(overViewList = overViewList) },
backgroundColor = Color(0xFFf2f2f2)
)
}
#Composable fun DisplayOverViewScreen(
modifier: Modifier = Modifier, overViewList: List<OverViewListItem>
) {
LazyColumn(modifier = modifier) {
items(overViewList) { data ->
when (data) {
is OverViewHeaderItem -> {
HeaderItem(data,overViewList)
}
}
}
}
}
HeaderItem Composable Function is below :
#Composable fun HeaderItem(overViewHeaderItem: OverViewHeaderItem,overViewList: List<OverViewListItem>) { <br>
var angle by remember {
mutableStateOf(0f)
}
var canDisplayChild by remember {
mutableStateOf(false)
}
**if(canDisplayChild){
HandleHistoryTodayChild(canDisplayChild = true,overViewList)
}**
when (overViewHeaderItem.listType) {
ItemType.IN_PROGRESS_HEADER -> {
Column(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(color = Color(0xffdc8633)),
verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.Start
) {
Text(
text = "In Progress", color = Color.White,
modifier = Modifier.padding(start = 16.dp)
)
}
}
ItemType.HISTORY_TODAY_HEADER -> {
Column(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(color = Color(0xffd7d7d7)),
verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.Start
) {
Text(
text = stringResource(R.string.history_label), color = Color.Black,
modifier = Modifier.padding(start = 16.dp)
)
}
}
else -> {
Row(modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(color = Color(0xffd7d7d7)),
horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(R.string.history_yesterday), color = Color.Black,
modifier = Modifier.padding(start = 16.dp)
)
Spacer(Modifier.weight(1f))
**Image(
painter = painterResource(id = R.drawable.ic_expand_more),
modifier = Modifier
.padding(end = 5.dp)
.rotate(angle)
.clickable {
angle = (angle + 180) % 360f
canDisplayChild = !canDisplayChild
},
contentDescription = "Expandable Image"
)**
}
}
}
}
Handle History info where recomposition is called
#Composable
fun HandleHistoryTodayChild(canDisplayChild:Boolean,overViewList: List<OverViewListItem>) {
if(canDisplayChild){
**PrepareOverViewScreen(overViewList = overViewList)**
}
}
Your problem should be:
Spacer(Modifier.weight(1f))
Try to specify a fixed height and work your solution from there.

Android compose, Indicator size problem with coil

Recently, I try to migrate to Jetpack compose.
So, I want to show 'card' where has 'indicator' while loading
but I can't change 'indicator' size in 'SubcomposeAsyncImage'
#Composable
private fun SponsorDialogContent(
imgUrl: String,
) {
Card(
modifier = Modifier
.width(300.dp)
.wrapContentHeight(),
elevation = CardDefaults.cardElevation(0.dp),
shape = RoundedCornerShape(10.dp)
) {
Box() {
SubcomposeAsyncImage(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.heightIn(min = 200.dp),
model = imgUrl,
contentDescription = null,
loading = {
CircularProgressIndicator(modifier = Modifier.size(30.dp).align(Alignment.Center), progress = 1f)
},
alignment = Alignment.Center,
contentScale = ContentScale.FillWidth
)
CircularProgressIndicator(1f, Modifier.width(30.dp).align(Alignment.Center))
}
}
}
So. I have try the code below
#Composable
private fun SponsorDialogContent(
imgUrl: String,
) {
Card(
modifier = Modifier
.width(300.dp)
.wrapContentHeight(),
elevation = CardDefaults.cardElevation(0.dp),
shape = RoundedCornerShape(10.dp)
) {
Box() {
SubcomposeAsyncImage(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.heightIn(min = 200.dp),
model = imgUrl,
contentDescription = null,
loading = {
Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
},
alignment = Alignment.Center,
contentScale = ContentScale.FillWidth
)
}
}
}
Then, works well, but I don't know why
Does anyone know why?
I tried to find how to work this code but failed
As #Jan Bina mentioned it has propagateMinConstraints = true which means it forces minimum constraints to its direct child only. I explained in this answer how it effects with Surface because Surface is a Box with propagateMinConstraints = true. You can try out these with Box and you will see the same outcomes
Surface(
modifier = Modifier.size(200.dp),
onClick = {}) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {}
}
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier.size(200.dp),
onClick = {}) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Green, RoundedCornerShape(6.dp))
) {}
}
}
Spacer(modifier = Modifier.height(20.dp))
Box(
modifier = Modifier.size(200.dp)
) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Green, RoundedCornerShape(6.dp))
) {}
}
}
In first example on Surface forces Column to have 200.dp size even though it has Modifier.size(50.dp).
In second example Box inside Column has 50.dp size because it's not a direct descendant of Surface.
In third example if we replace Surface(Box with propagateMinConstraints true) with Box it allows direct descendant to use its own constraints or dimensions.
And when you set Modifier.fillMaxWidth you set Constraints as minWidth = parent width, maxWidth = parentWidth
I answered about Constraint types here
If you browse the source code of SubcomposeAsyncImage, you will get to the point where you can see this:
BoxWithConstraints(
modifier = modifier, // this is the modifier you passed to SubcomposeAsyncImage
contentAlignment = alignment,
propagateMinConstraints = true,
) {
// image or your loading content based on state
}
So the behavior you describe is because of your modifier that has heightIn(min = 200.dp) and your progress indicator that ends up being direct child of this Box which has propagateMinConstraints = true.

Expandable FAB going through bottom nav bar - Jetpack Compose

I made an expandable floating action button like
here
i want to make it expand verticly and reveal to more buttons, when its note docked to bottom navigation bar, every thing is ok but when i make it docked it's going through bottom nav bar,
When not expanded looking like this
but when expanded like this
How can i make it expand properly
and i also want to align all 3 circle buttons horizontaly.
Thank you so much!
Code:
#Composable
fun ExpandableFAB(
modifier: Modifier = Modifier,
onFabItemClicked: (fabItem: ExpandableFABMenuItem) -> Unit,
menuItems: List<ExpandableFABMenuItem> = ExpandableFABMenuItem.expandableFabMenuItems
) {
var expandedState by remember { mutableStateOf(false) }
val rotationState by animateFloatAsState(
targetValue = if (expandedState) 45f else 0f
)
Column(
modifier = modifier.wrapContentSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
AnimatedVisibility(
visible = expandedState,
enter = fadeIn() + expandVertically(),
exit = fadeOut()
) {
LazyColumn(
modifier = Modifier.wrapContentSize(),
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(15.dp)
) {
items(menuItems.size) { index ->
ExpandableFABItem(
item = menuItems[index],
onFabItemClicked = onFabItemClicked
)
}
item {}
}
}
FloatingActionButton(
onClick = { expandedState = !expandedState },
backgroundColor = Blue
) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription = "FAB",
modifier = Modifier.rotate(rotationState),
)
}
}
}
#Composable
fun ExpandableFABItem(
item: ExpandableFABMenuItem,
onFabItemClicked: (item: ExpandableFABMenuItem) -> Unit
) {
Row(
modifier = Modifier
.wrapContentSize()
.padding(end = 10.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = item.label,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.background(MaterialTheme.colors.surface)
.padding(horizontal = 6.dp, vertical = 4.dp)
)
Box(
modifier = Modifier
.clip(CircleShape)
.size(42.dp)
.clickable { onFabItemClicked }
.background(Blue),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = item.icon,
contentDescription = "Create New Note",
tint = White
)
}
}
}

Align Box/Column to bottom of screen Jetpack Compose

I essentially want cards pinned to the top with a group of buttons pinned to the bottom (on screen keyboard)
Using Column with a modifier like so only leads to the buttons covering the top cards:
fun HomeScreen() {
Column(
modifier = Modifier
.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceAround
) {
WordGrid()
}
Column(
modifier = Modifier
.fillMaxWidth(),
verticalArrangement = Arrangement.Bottom
) {
Keyboard()
}
I have tried using all the different Arrangements, using a row and using Boxes, but can't seem to get it to work.
Curiously, in the #Preview it looks as though the above code works, but when ran on an emulator they are both at the top of the screen.
Using a spacer is another option, but would this cause issues in other ways? maybe with screen sizes etc?
If you want your buttons row to be pinned to the bottom, you have to set the Column to have a weight of 1f, something like this
MyTheme {
Surface(color = MyTheme.colors.background) {
// Cards content
Column(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.fillMaxWidth().weight(1f)
) {
Card(
modifier = Modifier.fillMaxWidth().height(80.dp).padding(all = 16.dp),
backgroundColor = Color.Red,
) {
Text(text = "Card 1")
}
Card(
modifier = Modifier.fillMaxWidth().height(80.dp).padding(all = 16.dp),
backgroundColor = Color.Green,
) {
Text(text = "Card 2")
}
Card(
modifier = Modifier.fillMaxWidth().height(80.dp).padding(all = 16.dp),
backgroundColor = Color.Blue,
) {
Text(text = "Card 3")
}
}
// Buttons content
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = {},
modifier = Modifier.padding(horizontal = 8.dp)
) {
Text(text = "Button 1")
}
Button(
onClick = {},
modifier = Modifier.padding(horizontal = 8.dp)
) {
Text(text = "Button 3")
}
Button(
onClick = {},
modifier = Modifier.padding(horizontal = 8.dp)
) {
Text(text = "Button 2")
}
}
}
}
}

Categories

Resources