Nikos Kermanidis
03/15/2022, 2:48 PMLazyColumn
witch displays the stocks and each stock is real-time updated through web-socket events.
I am testing the LazyColumn performance (using a release build) and it’s dropping frames. The performance is not as good as it was with the RecyclerView (with DiffUtil). I also used the Recomposition highlighter from the Google play team and verified that my LazyColumn is constantly recomposing. I will include the code in the thread. Any ideas on how to improve the performance?@Composable
private fun WatchlistColumn(
listState: LazyListState,
followedSecuritySecurities: FollowedSecurityListItems?,
...
) {
val stocks = followedSecuritySecurities?.eqty.orEmpty()
val etfs = followedSecuritySecurities?.etf.orEmpty()
val cryptos = followedSecuritySecurities?.crypto.orEmpty()
LazyColumn(
state = listState,
modifier = Modifier
.testTag("watchlist_column"),
) {
if (stocks.isNotEmpty()) {
// Add header
item(key = SecurityType.EQTY) {
WatchlistSectionHeader(
headerTitle = R.string.watchlist_header_stocks,
displayMode = displayMode,
onDisplayModeChanged = onDisplayModeChanged,
showDisplayMode = true
)
}
items(stocks, key = { listItem -> listItem.id }) {
WatchlistItem(
item = it,
displayMode = displayMode,
numberFormats = numberFormats,
onSecurityClick = onSecurityClick,
onDisplayModeChanged = onDisplayModeChanged,
onItemUnfollowed = onItemUnfollowed
)
}
}
...
The followedSecuritySecurities
comes from the ViewModel
val followedSecuritySecurities by viewModel.followedSecuritySecurities.collectAsState(initial = null)
In the viewModel:
private val _followedSecurities = MutableStateFlow<FollowedSecurities?>(null)
val followedSecuritySecurities = _followedSecurities.map {
it?.toFollowedListItems()
}
And this is the function in the viewModel that updates the list when the price of an item changes:
private fun handleFollowSecurityUpdate(securityQuote: SecurityQuote) {
val followedSecurities = _followedSecurities.value ?: return
val allFollowedSecurities = followedSecurities.all().toMutableList()
val stockIndex = allFollowedSecurities.indexOfFirst { it.security.subscribingId == securityQuote.id }
if (stockIndex == -1) {
return
}
val newFollowedStock = allFollowedSecurities[stockIndex].copyWithQuote(securityQuote)
allFollowedSecurities[stockIndex] = newFollowedStock
_followedSecurities.value = FollowedSecurities(allFollowedSecurities)
}
dead.fish
03/15/2022, 3:04 PMkey = { listItem -> listItem.id }
, maybe it’s just not the right one / stable one?Stylianos Gakis
03/15/2022, 3:05 PMNikos Kermanidis
03/15/2022, 3:07 PMStylianos Gakis
03/15/2022, 3:13 PMval stocks = followedSecuritySecurities?.eqty.orEmpty()
being defined in composition and basically happen on every recomposition. Might be stretch but what if you wrap each on of them like
val stocks = remember(followedSecuritySecurities.eqty) {
followedSecuritySecurities?.eqty.orEmpty()
}
Nikos Kermanidis
03/15/2022, 3:14 PMStylianos Gakis
03/15/2022, 3:15 PMcollectAsState
seems to used produceState
under the hood so shouldn’t be an issueNikos Kermanidis
03/15/2022, 3:16 PM@Composable
fun WatchlistScreen(
viewModel: WatchlistViewModel,
) {
val followedListItems by viewModel.followedListItems.collectAsState(initial = null)
WatchlistScreen(
listState = rememberLazyListState(),
isLoading = isLoading,
followedSecuritySecurities = followedListItems,
...
}
I wonder if this has to do with the lines val stocks = followedSecuritySecurities?.eqty.orEmpty() being defined in composition and basically happen on every recomposition.
I don’t see a difference when doing this unfortunately. I do think however that it is better to wrap it in a remember.Stylianos Gakis
03/15/2022, 3:30 PMtomoya0x00
03/15/2022, 3:38 PMPaul Woitaschek
03/15/2022, 5:18 PMritesh
03/15/2022, 7:22 PMVerified my lazycolumn is constantly recomposing
IMO this should not be happening
eygraber
03/15/2022, 9:57 PMLazyColumn
every time I use it. That is, unless each individual item observes it's own state, all visible items recompose when even one item in the list is updated.
I just assumed that that's how it works.Paul Woitaschek
03/15/2022, 10:21 PMmyanmarking
03/15/2022, 10:22 PMNikos Kermanidis
03/16/2022, 7:23 AMLazyColumn
accepts a Flow as a parameter:
@Composable
private fun WatchlistColumn(
listState: LazyListState,
followedSecuritySecurities: Flow<FollowedSecurityListItems>,
and unwraps the state itself, instead of unwrapping the state at the top level composable.
The other thing is marking my data classes as @Immutable
like @Paul Woitaschek suggested.
It seems to help, the recomposeHighlighter
is not red anymore.
However I placed some logs and it seems that a web socket update which updates a price of one single item, still triggers recomposition for all visible items. Before, when I was unwrapping the list of items at the top-level composable, it triggered recomposition for the LazyColumn
too.
I used this for logging:
class Ref(var value: Int)
// Note the inline function below which ensures that this function is essentially
// copied at the call site to ensure that its logging only recompositions from the
// original call site.
@Composable
inline fun LogCompositions(tag: String, msg: String) {
val ref = remember { Ref(0) }
SideEffect { ref.value++ }
Log.d(tag, "Compositions: $msg ${ref.value}")
}
This suggests to me that there is room for more optimisation.Security
model and pass the price through CompositionLocal
to the TextView that displays the price. This is very performant. It was tested with a real-time updated LazyColumn with 1000's of entries.
But it is something I would hope we don’t have to do forever. I am seeking for a simpler solution which is still performant.Paul Woitaschek
03/16/2022, 8:11 AMmyanmarking
03/16/2022, 9:20 AMNikos Kermanidis
03/16/2022, 9:21 AM@Immutable
data class FollowedSecurityListItem(
val followedSecurity: FollowedSecurity,
override val userAcceptedAgreements: Boolean
) : Identifiable, HideableSecurity {
override val id: String
get() = followedSecurity.id
override val security: Security
get() = followedSecurity.security
}
@Composable
private fun WatchlistItem(
item: FollowedSecurityListItem,
displayMode: SecurityDisplayMode?,
numberFormats: NumberFormats,
onSecurityClick: (FollowedSecurityListItem) -> Unit,
onDisplayModeChanged: () -> Unit,
onItemUnfollowed: (FollowedSecurityListItem) -> Unit
) {
val rememberedItem = remember(item) { item }
LogCompositions("Message", "WatchlistItem ${item.id}")
var itemDeleted by remember {
mutableStateOf(false)
}
val dismissState = rememberDismissState(
confirmStateChange = {
if (it == DismissValue.DismissedToStart) {
itemDeleted = true
}
true
}
)
val color by animateColorAsState(
targetValue = if (itemDeleted) {
MaterialTheme.colors.background
} else {
MaterialTheme.colors.backgroundGray
},
finishedListener = {
onItemUnfollowed(item)
}
)
SwipeToDismiss(
state = dismissState,
directions = setOf(DismissDirection.EndToStart),
dismissThresholds = { FractionalThreshold(0.15f) },
modifier = Modifier
.testTag("swipe_to_dismiss")
.recomposeHighlighter(),
background = {
WatchlistItemBackground(
modifier = Modifier.background(color = color)
)
},
dismissContent = {
WatchlistItemContent(rememberedItem, displayMode, numberFormats, onSecurityClick, onDisplayModeChanged)
}
)
}
Paul Woitaschek
03/16/2022, 9:22 AMmyanmarking
03/16/2022, 9:23 AMPaul Woitaschek
03/16/2022, 9:23 AMmyanmarking
03/16/2022, 9:24 AMNikos Kermanidis
03/16/2022, 9:25 AMSecurityDisplayMode
is an enum:
enum class SecurityDisplayMode {
VALUE,
PERFORMANCE_TODAY,
PERFORMANCE_ALL_TIME;
}
NumberFormats is a singleton with some helper function so that we can format the prices. I was thinking to provide it as a CompositionLocal instead of passing it down the tree. I don’t think that causes the performance problem though.myanmarking
03/16/2022, 9:26 AMNikos Kermanidis
03/16/2022, 9:26 AMremember the item is useless there i guess. It wont do anything, but it is almost sure not the cause
This is a recent addition. Before I didn’t use remember
there. I thought it might help.Paul Woitaschek
03/16/2022, 9:26 AMobject Counter { var count : Int }
Nikos Kermanidis
03/16/2022, 9:28 AMPaul Woitaschek
03/16/2022, 9:29 AMmyanmarking
03/16/2022, 9:29 AMNikos Kermanidis
03/16/2022, 9:30 AManimateItemPlacement
modifier. There is an animation when the price changes though. But we had the same performance issue with other similar long lists that didn’t have any animations.myanmarking
03/16/2022, 9:30 AMPaul Woitaschek
03/16/2022, 9:31 AMmyanmarking
03/16/2022, 9:31 AMNikos Kermanidis
03/16/2022, 9:33 AMideally, the data that you use to configure the composables should be already formatted/mapped
You are right. I will work on that. However I have the feeling that this doesn’t cause the recompositions.
This Singleton doesn’t contain any state. Just helper functions.Paul Woitaschek
03/16/2022, 9:34 AMNikos Kermanidis
03/16/2022, 9:35 AM@Immutable
while it isn’t?myanmarking
03/16/2022, 9:36 AMPaul Woitaschek
03/16/2022, 9:37 AMmyanmarking
03/16/2022, 9:37 AMPaul Woitaschek
03/16/2022, 9:42 AMmyanmarking
03/16/2022, 9:43 AMNikos Kermanidis
03/16/2022, 9:46 AMPaul Woitaschek
03/16/2022, 9:46 AMmyanmarking
03/16/2022, 9:47 AMNikos Kermanidis
03/16/2022, 9:47 AMmyanmarking
03/16/2022, 9:48 AMNikos Kermanidis
03/16/2022, 9:50 AMThen slap @Immutable on for a first try
Wow @Paul Woitaschek that seems to work!myanmarking
03/16/2022, 9:50 AMNikos Kermanidis
03/16/2022, 9:51 AMmyanmarking
03/16/2022, 10:12 AMPaul Woitaschek
03/16/2022, 10:14 AMmyanmarking
03/16/2022, 10:17 AMNikos Kermanidis
03/16/2022, 10:21 AMOne notable type that is stable but _is_ mutable is Compose's MutableState type. If a value is held in a MutableState, the state object overall is considered to be stable as Compose will be notified of any changes to the .value property of State.
I think @Immutable
is a stronger version of @Stable
.@Immutable
annotation to all your models.myanmarking
03/16/2022, 10:24 AMdata class Item(val date: LocalDate)
data class Item(val date: LocalDate)
@Composable
fun ItemComposable(item: Item){
otherComposable(item.date)
}
@Composable
fun otherComposable(date: LocalDate){}
As it is, it will recompose ItemComposable regardless os date being the same, but it will skip otherComposable. With stable, it will skip completely.Paul Woitaschek
03/16/2022, 11:00 AMmyanmarking
03/16/2022, 11:03 AMPaul Woitaschek
03/16/2022, 11:04 AMmyanmarking
03/16/2022, 11:08 AMNikos Kermanidis
03/16/2022, 11:11 AM