performance: reduce recompositons in PersonCard and SeasonCard (#1184)

<!-- By submitting this pull request, you acknowledge that you have read
the [contributing
guide](https://github.com/damontecres/Wholphin/blob/main/CONTRIBUTING.md,
including the AI/LLM policy, and [developer's
guide](https://github.com/damontecres/Wholphin/blob/main/DEVELOPMENT.md)
-->

## Description
<!-- Describe the changes in detail -->
related docs:
https://developer.android.com/develop/ui/compose/performance/bestpractices#defer-reads
Removed setting padding with animated value, instead moved to lambda
offset and layout modifiers to increase performance and reduce
recompositions when focusing those cards

Split from #1152

### Related issues
<!-- If this is a new feature or a change, there must be a discussion in
an issue first, reference the issue here -->
<!-- If fixing a bug, reference the bug issue here, or describe the bug,
including steps to reproduce -->

### Testing
<!-- Describe how this change was tested and on what device(s) -->
Tested on a emulato
## Screenshots
<!-- Please include screenshots if the PR alters any UI elements -->
|Component | Pre | Post |
|---|--------|--------|
|Person Card|<img width="404" height="366" alt="Screenshot 2026-04-02 at
08 19 18"
src="https://github.com/user-attachments/assets/73b38c75-040b-401e-88d6-4bfc9ffc0cc9"
/> | <img width="407" height="346" alt="Screenshot 2026-04-02 at 08 22
06"
src="https://github.com/user-attachments/assets/add473f7-dea6-4e33-97b8-6954149a81e2"
/> |
|Season Card| <img width="404" height="341" alt="Screenshot 2026-04-02
at 08 18 31"
src="https://github.com/user-attachments/assets/eec594ee-6701-4f99-8fb3-339ed8559ec4"
/> |<img width="408" height="299" alt="Screenshot 2026-04-02 at 08 24
06"
src="https://github.com/user-attachments/assets/8de445a6-dcb8-4c7e-a5f6-a25bf822c822"
/> |



## AI or LLM usage
<!-- If you used any AI or LLM assistance, please list where in the code
and how you tested it -->
Gemini in AS was used for repetitive code completion
This commit is contained in:
Leon Omelan 2026-04-06 16:11:01 +02:00 committed by GitHub
parent 44d1ac484f
commit 5ab4bb712b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 40 additions and 12 deletions

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
@ -25,9 +26,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.layout
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Border
@ -80,7 +83,7 @@ fun PersonCard(
) {
val hideOverlayDelay = 1_000L
val focused = interactionSource.collectIsFocusedAsState().value
val focused by interactionSource.collectIsFocusedAsState()
var focusedAfterDelay by remember { mutableStateOf(false) }
if (focused) {
@ -95,10 +98,13 @@ fun PersonCard(
} else {
focusedAfterDelay = false
}
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp)
// Do not use `by` here, this way we are Defer reads and recompositions to only when modifier calculates
val spaceBetweenState = animateDpAsState(if (focused) 12.dp else 4.dp, label = "spaceBetween")
val spaceBelowState = animateDpAsState(if (focused) 4.dp else 12.dp, label = "spaceBelow")
Column(
verticalArrangement = Arrangement.spacedBy(spaceBetween),
verticalArrangement = Arrangement.spacedBy(4.dp), // Fixed base spacing
modifier = modifier,
) {
Card(
@ -168,8 +174,16 @@ fun PersonCard(
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier =
Modifier
.padding(bottom = spaceBelow)
.fillMaxWidth(),
// Optimization: move animation reads to layout/draw phase
.offset {
IntOffset(0, (spaceBetweenState.value - 4.dp).roundToPx())
}.layout { measurable, constraints ->
val paddingPx = spaceBelowState.value.roundToPx()
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height + paddingPx) {
placeable.placeRelative(0, 0)
}
}.fillMaxWidth(),
) {
Text(
text = name ?: "",

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
@ -18,10 +19,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
@ -52,7 +55,7 @@ fun SeasonCard(
val imageUrlService = LocalImageUrlService.current
val density = LocalDensity.current
val imageUrl =
remember(item, imageHeight, imageWidth) {
remember(item, imageHeight, imageWidth, density) {
if (item != null) {
val fillHeight =
if (imageHeight != Dp.Unspecified) {
@ -125,14 +128,17 @@ fun SeasonCard(
aspectRatio: Float = AspectRatios.TALL,
) {
val focused by interactionSource.collectIsFocusedAsState()
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp)
// Do not use `by` here, this way we are Defer reads and recompositions to only when modifier calculates
val spaceBetween = animateDpAsState(if (focused) 12.dp else 4.dp, label = "spaceBetween")
val spaceBelow = animateDpAsState(if (focused) 4.dp else 12.dp, label = "spaceBelow")
val focusedAfterDelay by rememberFocusedAfterDelay(interactionSource)
val aspectRationToUse = aspectRatio.coerceAtLeast(AspectRatios.MIN)
val width = imageHeight * aspectRationToUse
val height = imageWidth * (1f / aspectRationToUse)
Column(
verticalArrangement = Arrangement.spacedBy(spaceBetween),
verticalArrangement = Arrangement.spacedBy(4.dp), // Fixed base spacing
modifier = modifier.size(width, height),
) {
Card(
@ -173,8 +179,16 @@ fun SeasonCard(
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier =
Modifier
.padding(bottom = spaceBelow)
.fillMaxWidth(),
// Optimization: move animation reads to layout/draw phase
.offset {
IntOffset(0, (spaceBetween.value - 4.dp).roundToPx())
}.layout { measurable, constraints ->
val paddingPx = spaceBelow.value.roundToPx()
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height + paddingPx) {
placeable.placeRelative(0, 0)
}
}.fillMaxWidth(),
) {
Text(
text = title ?: "",