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?Nikos Kermanidis
03/15/2022, 2:54 PM@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)
}
Nikos Kermanidis
03/15/2022, 3:01 PMdead.fish
03/15/2022, 3:04 PMkey = { listItem -> listItem.id }
, maybe it’s just not the right one / stable one?dead.fish
03/15/2022, 3:05 PMStylianos Gakis
03/15/2022, 3:05 PMStylianos Gakis
03/15/2022, 3:06 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,
...
}
Nikos Kermanidis
03/15/2022, 3:26 PMI 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 PMPaul Woitaschek
03/15/2022, 5:19 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.Nikos Kermanidis
03/16/2022, 8:09 AMSecurity
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
}
Nikos Kermanidis
03/16/2022, 9:21 AM@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 AMmyanmarking
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 AMmyanmarking
03/16/2022, 9:32 AMmyanmarking
03/16/2022, 9:33 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 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 AMmyanmarking
03/16/2022, 9:39 AMPaul Woitaschek
03/16/2022, 9:42 AMmyanmarking
03/16/2022, 9:43 AMmyanmarking
03/16/2022, 9:43 AMmyanmarking
03/16/2022, 9:44 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 AMmyanmarking
03/16/2022, 9:50 AMNikos Kermanidis
03/16/2022, 9:51 AMmyanmarking
03/16/2022, 10:12 AMmyanmarking
03/16/2022, 10:13 AMPaul Woitaschek
03/16/2022, 10:14 AMmyanmarking
03/16/2022, 10:17 AMmyanmarking
03/16/2022, 10:19 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
.Nikos Kermanidis
03/16/2022, 10:23 AM@Immutable
annotation to all your models.myanmarking
03/16/2022, 10:24 AMmyanmarking
03/16/2022, 10:25 AMdata class Item(val date: LocalDate)
myanmarking
03/16/2022, 10:26 AMmyanmarking
03/16/2022, 10:26 AMmyanmarking
03/16/2022, 10:34 AMdata 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 AMmyanmarking
03/16/2022, 11:03 AMPaul Woitaschek
03/16/2022, 11:04 AMPaul Woitaschek
03/16/2022, 11:04 AMmyanmarking
03/16/2022, 11:08 AMNikos Kermanidis
03/16/2022, 11:11 AM