https://kotlinlang.org logo
Channels
100daysofcode
100daysofkotlin
100daysofkotlin-2021
advent-of-code
aem
ai
alexa
algeria
algolialibraries
amsterdam
android
android-architecture
android-databinding
android-studio
androidgithubprojects
androidthings
androidx
androidx-xprocessing
anime
anko
announcements
apollo-kotlin
appintro
arabic
argentina
arkenv
arksemdevteam
armenia
arrow
arrow-contributors
arrow-meta
ass
atlanta
atm17
atrium
austin
australia
austria
awesome-kotlin
ballast
bangladesh
barcelona
bayarea
bazel
beepiz-libraries
belgium
benchmarks
berlin
big-data
books
boston
brazil
brikk
budapest
build
build-tools
bulgaria
bydgoszcz
cambodia
canada
carrat
carrat-dev
carrat-feed
chicago
chile
china
chucker
cincinnati-user-group
cli
clikt
cloudfoundry
cn
cobalt
code-coverage
codeforces
codemash-precompiler
codereview
codingame
codingconventions
coimbatore
collaborations
colombia
colorado
communities
competitive-programming
competitivecoding
compiler
compose
compose-android
compose-desktop
compose-hiring
compose-ios
compose-mp
compose-ui-showcase
compose-wear
compose-web
confetti
connect-audit-events
corda
cork
coroutines
couchbase
coursera
croatia
cryptography
cscenter-course-2016
cucumber-bdd
cyprus
czech
dagger
data2viz
databinding
datascience
dckotlin
debugging
decompose
decouple
denmark
deprecated
detekt
detekt-hint
dev-core
dfw
docs-revamped
dokka
domain-driven-design
doodle
dsl
dublin
dutch
eap
eclipse
ecuador
edinburgh
education
effective-kotlin
effectivekotlin
emacs
embedded-kotlin
estatik
event21-community-content
events
exposed
failgood
fb-internal-demo
feed
firebase
flow
fluid-libraries
forkhandles
forum
fosdem
fp-in-kotlin
framework-elide
freenode
french
fritz2
fuchsia
functional
funktionale
gamedev
ge-kotlin
general-advice
georgia
geospatial
german-lang
getting-started
github-workflows-kt
glance
godot-kotlin
google-io
gradle
graphic
graphkool
graphql
graphql-kotlin
graviton-browser
greece
grpc
gsoc
gui
hackathons
hacktoberfest
hamburg
hamkrest
helios
helsinki
hexagon
hibernate
hikari-cp
hire-me
hiring
hongkong
hoplite
http4k
hungary
hyderabad
image-processing
india
indonesia
inkremental
intellij
intellij-plugins
intellij-tricks
internships
introduce-yourself
io
ios
iran
israel
istanbulcoders
italian
jackson-kotlin
jadx
japanese
jasync-sql
java-to-kotlin-refactoring
javadevelopers
javafx
javalin
javascript
jdbi
jhipster-kotlin
jobsworldwide
jpa
jshdq
juul-libraries
jvm-ir-backend-feedback
jxadapter
k2-early-adopters
kaal
kafka
kakao
kalasim
kapt
karachi
karg
karlsruhe
kash_shell
kaskade
kbuild
kdbc
kgen-doc-tools
kgraphql
kinta
klaxon
klock
kloudformation
kmdc
kmm-español
kmongo
knbt
knote
koalaql
koans
kobalt
kobweb
kodein
kodex
kohesive
koin
koin-dev
komapper
kondor-json
kong
kontent
kontributors
korau
korean
korge
korim
korio
korlibs
korte
kotest
kotest-contributors
kotless
kotlick
kotlin-asia
kotlin-beam
kotlin-by-example
kotlin-csv
kotlin-data-storage
kotlin-foundation
kotlin-fuel
kotlin-in-action
kotlin-inject
kotlin-latam
kotlin-logging
kotlin-multiplatform-contest
kotlin-mumbai
kotlin-native
kotlin-pakistan
kotlin-plugin
kotlin-pune
kotlin-roadmap
kotlin-samples
kotlin-sap
kotlin-serbia
kotlin-spark
kotlin-szeged
kotlin-website
kotlinacademy
kotlinbot
kotlinconf
kotlindl
kotlinforbeginners
kotlingforbeginners
kotlinlondon
kotlinmad
kotlinprogrammers
kotlinsu
kotlintest
kotlintest-devs
kotlintlv
kotlinultimatechallenge
kotlinx-datetime
kotlinx-files
kotlinx-html
kotrix
kotson
kovenant
kprompt
kraph
krawler
kroto-plus
ksp
ktcc
ktfmt
ktlint
ktor
ktp
kubed
kug-leads
kug-torino
kvision
kweb
lambdaworld_cadiz
lanark
language-evolution
language-proposals
latvia
leakcanary
leedskotlinusergroup
lets-have-fun
libgdx
libkgd
library-development
lincheck
linkeddata
lithuania
london
losangeles
lottie
love
lychee
macedonia
machinelearningbawas
madrid
malaysia
mathematics
meetkotlin
memes
meta
metro-detroit
mexico
miami
micronaut
minnesota
minutest
mirror
mockk
moko
moldova
monsterpuzzle
montreal
moonbean
morocco
motionlayout
mpapt
mu
multiplatform
mumbai
munich
mvikotlin
mvrx
myndocs-oauth2-server
naming
navigation-architecture-component
nepal
new-mexico
new-zealand
newname
nigeria
nodejs
norway
npm-publish
nyc
oceania
ohio-kotlin-users
oldenburg
oolong
opensource
orbit-mvi
osgi
otpisani
package-search
pakistan
panamá
pattern-matching
pbandk
pdx
peru
philippines
phoenix
pinoy
pocketgitclient
polish
popkorn
portugal
practical-functional-programming
proguard
prozis-android-backup
pyhsikal
python
python-contributors
quasar
random
re
react
reaktive
realm
realworldkotlin
reductor
reduks
redux
redux-kotlin
refactoring-to-kotlin
reflect
refreshversions
reports
result
rethink
revolver
rhein-main
rocksdb
romania
room
rpi-pico
rsocket
russian
russian_feed
russian-kotlinasfirst
rx
rxjava
san-diego
science
scotland
scrcast
scrimage
script
scripting
seattle
serialization
server
sg-user-group
singapore
skia-wasm-interop-temp
skrape-it
slovak
snake
sofl-user-group
southafrica
spacemacs
spain
spanish
speaking
spek
spin
splitties
spotify-mobius
spring
spring-security
squarelibraries
stackoverflow
stacks
stayhungrystayfoolish
stdlib
stlouis
strife-discord-lib
strikt
students
stuttgart
sudan
swagger-gradle-codegen
swarm
sweden
swing
swiss-user-group
switzerland
talking-kotlin
tallinn
tampa
teamcity
tegal
tempe
tensorflow
terminal
test
testing
testtestest
texas
tgbotapi
thailand
tornadofx
touchlab-tools
training
tricity-kotlin-user-group
trójmiasto
truth
tunisia
turkey
turkiye
twitter-feed
uae
udacityindia
uk
ukrainian
uniflow
unkonf
uruguay
utah
uuid
vancouver
vankotlin
vertx
videos
vienna
vietnam
vim
vkug
vuejs
web-mpp
webassembly
webrtc
wimix_sentry
wwdc
zircon
Powered by
Title
n

Nikos Kermanidis

03/15/2022, 2:48 PM
Hi! I am working on a stock trading application and we are currently migrating our real-time updated lists to compose. I have a
LazyColumn
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?
👍 3
This is my `LazyColumn`:
@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)
}
How can I make sure that when one item is updated through web-socket event, only that item recomposes, not the whole list? Basically replicating the DiffUtil behavior.
d

dead.fish

03/15/2022, 3:04 PM
Just sub here, but I think you’re already doing what’s neccessary, i.e. giving each item an unique ID via
key = { listItem -> listItem.id }
, maybe it’s just not the right one / stable one?
s

Stylianos Gakis

03/15/2022, 3:05 PM
When the entire list updates, if the LazyList is provided unique ids it should be able to do this efficiently. Are the ids provided unique?
Oh you beat me to it 😅
😄 1
n

Nikos Kermanidis

03/15/2022, 3:07 PM
The id’s provided are the id’s from the stocks themselves so yes unique. Each stock has a unique id.
s

Stylianos Gakis

03/15/2022, 3:13 PM
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. Might be stretch but what if you wrap each on of them like
val stocks = remember(followedSecuritySecurities.eqty) {
  followedSecuritySecurities?.eqty.orEmpty()
}
n

Nikos Kermanidis

03/15/2022, 3:14 PM
Thanks for the suggestion! I will give it a try.
s

Stylianos Gakis

03/15/2022, 3:15 PM
I was also thinking potentially you’re recreating the flow in composition, but
collectAsState
seems to used
produceState
under the hood so shouldn’t be an issue
n

Nikos Kermanidis

03/15/2022, 3:16 PM
I am unwrapping the state at the top level composable:
@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.
s

Stylianos Gakis

03/15/2022, 3:30 PM
Maybe a good question is, what is it that is recomposing that you’re not expecting to? Do all individual items recompose when only the data of one of them changes? If it’s the entire LazyColumn doing a recomposition when 1 item changes, maybe that’s expected? And it’s performant since it depends on not having all the children recompose? Just an idea, not sure how LazyList really works
t

tomoya0x00

03/15/2022, 3:38 PM
If you are instantiating some classes in WatchlistItem, it is better to reduce them. Recently, I worked on improving the performance of LazyColumn on a Nexus 7 2013. I am caching and reusing instances of Modifier, it is effective to improve the LazyColumn performance. Sample implementation: (Sorry in japanese) https://twitter.com/tomoya0x00/status/1503699937959477253
p

Paul Woitaschek

03/15/2022, 5:18 PM
Annotate your security model as Immutable
💯 1
r

ritesh

03/15/2022, 7:22 PM
Verified my lazycolumn is constantly recomposing
IMO this should not be happening
e

eygraber

03/15/2022, 9:57 PM
I've seen this with
LazyColumn
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.
p

Paul Woitaschek

03/15/2022, 10:21 PM
It doesn't have to work like this, read my comment on immutable
m

myanmarking

03/15/2022, 10:22 PM
The itens must be stable. The list must be stable. And the clicklisteners must not capture values. If this happens, it will completly skip the non changed items. Try to set empty listeners. Does the problem go away?
n

Nikos Kermanidis

03/16/2022, 7:23 AM
I did a couple of things which seem to help: Unwrap my state at the lowest level possible. So my
LazyColumn
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.
Is this expected? That an update to one item’s price, triggers recomposition for all visible items? We have implemented a workaround for this. We isolated the price from the
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.
p

Paul Woitaschek

03/16/2022, 8:11 AM
Please post the data model and the composable that is recomposing
m

myanmarking

03/16/2022, 9:20 AM
you dont need to do any of that, read the flow directly before the lazyColumn. The problem is elsewhere. Can you paste your model here please, it would help
n

Nikos 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
}
And the list item composable:
@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)
        }
    )
}
p

Paul Woitaschek

03/16/2022, 9:22 AM
Did by any chance you enable animateItemPlacement?
m

myanmarking

03/16/2022, 9:23 AM
indeed. I have noticed as well that can cause unexpected recompositions
p

Paul Woitaschek

03/16/2022, 9:23 AM
Are NumberFormats and SecurityDisplayMode Immutable as well?
m

myanmarking

03/16/2022, 9:24 AM
remember the item is useless there i guess. It wont do anything, but it is almost sure not the cause
n

Nikos Kermanidis

03/16/2022, 9:25 AM
No they are not.
SecurityDisplayMode
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.
m

myanmarking

03/16/2022, 9:26 AM
enum is stable
singleton probably is not!!
n

Nikos Kermanidis

03/16/2022, 9:26 AM
remember 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.
p

Paul Woitaschek

03/16/2022, 9:26 AM
Yep you can write
object Counter { var count : Int }
n

Nikos Kermanidis

03/16/2022, 9:28 AM
Is it a good idea to provide the Singleton (NumberFormats) as a CompositionLocal instead of passing it down the tree?
p

Paul Woitaschek

03/16/2022, 9:29 AM
No, that should not be abused for sneaking in method parameters
m

myanmarking

03/16/2022, 9:29 AM
won’t do any good. What you should do is make sure the data that is used in the composables is already formatted and mapped
n

Nikos Kermanidis

03/16/2022, 9:30 AM
I didn’t enable the
animateItemPlacement
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.
m

myanmarking

03/16/2022, 9:30 AM
if you can, extract the format stuff to viewmodel. If you cannot, try to make the formatter an interface and mark is as stable (if it is the case)
p

Paul Woitaschek

03/16/2022, 9:31 AM
If it’s not stable, it’s expected to cause recompositions because if it changes, all items that depend on it should change too
👍 1
m

myanmarking

03/16/2022, 9:31 AM
ideally, the data that you use to configure the composables should be already formatted/mapped
run the gradle command and it will tell you right away - it needs to be skippable and restartable, then you are fine
the formatter probably is unstable - that should be the apparent reason
n

Nikos Kermanidis

03/16/2022, 9:33 AM
ideally, 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.
p

Paul Woitaschek

03/16/2022, 9:34 AM
Then slap @Immutable on for a first try
👍 1
n

Nikos Kermanidis

03/16/2022, 9:35 AM
Is there a gradle command that tells you if a class is stable?
@Paul Woitaschek what will happen if you annotate a class as
@Immutable
while it isn’t?
m

myanmarking

03/16/2022, 9:36 AM
then it should work while being broken 😛
p

Paul Woitaschek

03/16/2022, 9:37 AM
It will not recompose and update properly
m

myanmarking

03/16/2022, 9:37 AM
not necessarily. It would depend i guess
p

Paul Woitaschek

03/16/2022, 9:42 AM
I could never get that one working
m

myanmarking

03/16/2022, 9:43 AM
In my experience you should check in this order: All params of the function are stable. The model class is immutable. There is a recompose scope there. Any listeners should not access something that is not stable
🧠 1
Normally, i solve recompose problems like above
The last one is optional i guess. You cannot always have that. For instance, imagine you want to scroll something on a button click. Then you would need to use a scope, which isn’t stable, so it causes some recomposition issues, which is completely normal in that case i guess
n

Nikos Kermanidis

03/16/2022, 9:46 AM
I miss the DiffUtil
p

Paul Woitaschek

03/16/2022, 9:46 AM
I really don’t 😄
👍 4
👍🏼 1
m

myanmarking

03/16/2022, 9:47 AM
did you try @Paul Woitaschek approach, did it work ?
n

Nikos Kermanidis

03/16/2022, 9:47 AM
I am working on formatting the prices in the viewModel and see if that helps.
m

myanmarking

03/16/2022, 9:48 AM
ya, the idea is to get rid of the singleton 😛 It should help
n

Nikos Kermanidis

03/16/2022, 9:50 AM
Then slap @Immutable on for a first try
Wow @Paul Woitaschek that seems to work!
m

myanmarking

03/16/2022, 9:50 AM
you should have params to format the composable, not the data!
ya, the singleton is not stable as we suspected
n

Nikos Kermanidis

03/16/2022, 9:51 AM
Thank you both! And everyone else who helped me troubleshooting this.
m

myanmarking

03/16/2022, 10:12 AM
@Paul Woitaschek is not a solution. Just a test to check if the singleton is the problem 😮
i don’t think you should do that, only if you are sure its indeed stable. On a side note, if the singleton is indeed stable, it should me marked as Stable and not Immutable. I think it is more appropriate
p

Paul Woitaschek

03/16/2022, 10:14 AM
I did not understand from the docs when you should use one over the other
m

myanmarking

03/16/2022, 10:17 AM
Me neither. I don’t think it matters, but the name should mean something. Immutable i think is for models. In this case, does it make sense to mark the singleton as immutable? Probably not. For me is makes more sense to make it Stable
which is the problem here. The excess recomposition in this case happens because his singleton is not stable (which has nothing to do with it being a singleton. A simple class there wouldn’t also be stable in the same scenario
n

Nikos Kermanidis

03/16/2022, 10:21 AM
From the docs:
One 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
.
Do you all think those annotations are here to stay? I feel it is a bit of a guesswork at least for me. It’s an extra thing you have to think about, adding the
@Immutable
annotation to all your models.
m

myanmarking

03/16/2022, 10:24 AM
you dont need to add to all the model. Just the ones that the compiler will define as unstable but they should be stable
example:
data class Item(val date: LocalDate)
this is unstable. But it should be stable because LocalDate is immutable. But the compiler won’t guess that. So you should mark it
most of the cases you won’t need to annotate
More clearly:
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.
p

Paul Woitaschek

03/16/2022, 11:00 AM
m

myanmarking

03/16/2022, 11:03 AM
exactly. That is the purpose of those annotations
probably, File equals can differentiate the same File. But as the class is not Stable, it will recompose either way
p

Paul Woitaschek

03/16/2022, 11:04 AM
Which makes sense as the file itself can change its contents
But it would be nice if you could for example slap an annotation to a composable function parameter instead of that ceremony
m

myanmarking

03/16/2022, 11:08 AM
ya, for sure!
n

Nikos Kermanidis

03/16/2022, 11:11 AM
@myanmarking Thanks for the explanation.