Compare commits

...
Author SHA1 Message Date
Claude d5223fa5db perf: fix Compose recomposition and flow-leak hot spots
- CardFeedView: use derivedStateOf for isHighlighted so only the
  affected item recomposes on highlight change, not all visible cards;
  switch contentType from javaClass.simpleName (reflection) to item::class
- HomeScreen live-bubbles LazyRow: replace hashCode() key with stable
  channel-id strings and add contentType for proper slot recycling
- UserProfilePicture: wrap room.users.toList() in remember(room) to
  avoid a list allocation every recomposition
- ThreadFeedView / SoftwareAppDetailScreen: remember(item) the
  levelFlowForItem flow so it isn't recreated on every recompose
- ShowDonationCard: remember the observeDonatedInThisVersion() StateFlow
  to prevent a new Eagerly coroutine being started on every recompose
2026-06-22 20:16:44 +00:00
6 changed files with 19 additions and 7 deletions
@@ -232,7 +232,8 @@ fun NonClickableUserPictures(
size: Dp,
accountViewModel: AccountViewModel,
) {
NonClickableUserPictures(room.users.toList(), size, accountViewModel)
val userList = remember(room) { room.users.toList() }
NonClickableUserPictures(userList, size, accountViewModel)
}
@Composable
@@ -476,7 +476,16 @@ fun DisplayLiveBubbles(
val feed by liveFeed.feed.collectAsStateWithLifecycle()
LazyRow(HorzPadding, horizontalArrangement = spacedBy(Size5dp)) {
itemsIndexed(feed.list, key = { _, item -> item.hashCode() }) { _, item ->
itemsIndexed(
feed.list,
key = { _, item ->
when (item) {
is EphemeralChatChannel -> item.roomId.id
is LiveActivitiesChannel -> item.address.toValue()
}
},
contentType = { _, item -> item::class },
) { _, item ->
when (item) {
is EphemeralChatChannel -> RenderEphemeralBubble(item, accountViewModel, nav)
is LiveActivitiesChannel -> RenderLiveActivityBubble(item, accountViewModel, nav)
@@ -44,6 +44,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -232,9 +233,9 @@ private fun FeedLoaded(
itemsIndexed(
items = items.list,
key = { _, item -> item.id() },
contentType = { _, item -> item.javaClass.simpleName },
contentType = { _, item -> item::class },
) { _, item ->
val isHighlighted = highlightedCardId == item.id()
val isHighlighted by remember(item) { derivedStateOf { highlightedCardId == item.id() } }
val highlightColor by animateColorAsState(
targetValue = if (isHighlighted) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) else Color.Transparent,
animationSpec = tween(durationMillis = if (isHighlighted) 300 else 1000),
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.donations
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.ui.components.LoadNote
@@ -34,7 +35,7 @@ fun ShowDonationCard(
nav: INav,
) {
if (!accountViewModel.account.hasDonatedInThisVersion()) {
val donated by accountViewModel.account.observeDonatedInThisVersion().collectAsStateWithLifecycle()
val donated by remember { accountViewModel.account.observeDonatedInThisVersion() }.collectAsStateWithLifecycle()
if (!donated) {
LoadNote(
BuildConfig.RELEASE_NOTES_ID,
@@ -331,7 +331,7 @@ private fun SoftwareAppDetailBody(
key = { _, item -> item.idHex },
contentType = { _, _ -> "comment" },
) { _, item ->
val level = threadViewModel.levelFlowForItem(item).collectAsStateWithLifecycle(0)
val level = remember(item) { threadViewModel.levelFlowForItem(item) }.collectAsStateWithLifecycle(0)
NoteCompose(
baseNote = item,
@@ -456,7 +456,7 @@ fun RenderThreadFeed(
}
},
) { index, item ->
val level = viewModel.levelFlowForItem(item).collectAsStateWithLifecycle(0)
val level = remember(item) { viewModel.levelFlowForItem(item) }.collectAsStateWithLifecycle(0)
val modifier =
Modifier