I'm working on chat UI. I'm looking at `Jetchat` s...
# compose
u
I'm working on chat UI. I'm looking at
Jetchat
sample I want to keep scroll at 0 (if it was at 0) when new messages are inserted
Copy code
val listState = rememberLazyListState()
SideEffect {
    if (!listState.canScrollBackward) {
        listState.requestScrollToItem(index = 0) <------------------
    }
}
LazyColumn(
    ...
    state = listState,
    ...
) {
    ...
}
this seems to do the trick. However, I also have a next screen (image message detail). If I navigate there, and then back, the chat screen composable is "instantiated" again, therefore triggering
SideEffect
lambda, and resetting the scroll -- which is not what I want -- the scroll offset should be kept (which it does by default, but then I don't get that "keep scroll at 0" behavior) Any clean way around this other than hacks?
b
*i am not guaranteing this will work in your case. it just solves a lot of problems for me around recompositions and effects firing multiple times!! but not always
Copy code
val uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
fun ui(block: suspend CoroutineScope.() -> Unit) = uiScope.launch(block)
try replacing the effect you have with the ui{ } dsl above. see if it fires only once and if it fixes your problem
u
hm how does that help? if I need to call
ui
instead of
SideEffect
, then it also gets executed, no?
b
your effect fires automatically after each recomposition
ui dsl above does not
should not, i dont even know anymore 🙂 i'm sory if it doesnt work for you
u
yea I need to dig deeper
b
you can rig the ui event safely and it wont fire multiple times. when your user comes back to the list somehow. thats all i am suggesting. rhe effects in compose sometimes fire multiple times
u
it seems to me that sideeffect fires when the composable gets recomposed - which is what the
listState.requestScrollToItem
requires, to run on each composition
so when its logical it runs again if navigation brings it back (back press)
so I need to selectively not do it.. i.e. to skip it if this is the first pass
b
you got me there 🙂 dont know, its a reverse side effect or something. Hey if this is too difficult why not make a togleable "stick to top" or "stick to bottom" widget like in intellij, the one that keeps the logger to the last line. even tho you want the first line. maybe that's easier and it gives control to the user back.
a
There’s a fun trick you can try here - if you have an empty
1px
high item as your bottom-most item, then when you are completely scrolled to the bottom, any new messages that come in will slot in right above that bottom-most item empty item, and you’ll naturally stay pinned to the bottom and you shouldn’t need any direct scroll calls. If you scroll away from the bottom (and therefore the scroll position is at any other point in the list) then you’ll keep that position when new items show up.
đź‘€ 1
u
thats funny, so it has to have stable id and content type, i.e. a proper lazycolumn item?
a
Yep exactly, still a normal
item
, but one that exists just to take a tiny amount of space for this behavior. I’ve done something like this:
Copy code
item(key = "KeepScrolledToBottom") {
                // Trick to keep scrolled to the bottom only if we are at the bottom
                // Having a 1px high item at the very start (therefore bottom with reverseLayout)
                // will be the current scroll position if and only if we are scrolled to the
                // very bottom
                Spacer(Modifier.height(with(LocalDensity.current) { 1f.toDp() }))
            }
            items(inputEventInfos.reversed(), key = InputEventInfo::id) { 
                // actual items
            }
u
neat, will try btw what about jetchat way, the sideffect + requestScrollToItem, is it unfixable?
a
You can probably make it work with a lot more accounting - trying to keep track of if you should automatically scroll based on different factors, and then use triggers that make sense for when you want to perform the scrolling in reaction to something. You can do things like “run this code, but not on initial composition” if you keep around a simple
var isFirstComposition by remember { mutablesStateOf(true) }
that you set to
false
at an appropriate time later
u
but in general, remembers are scoped to composotion, and when navigation to next screen, that dies, right? yet lazycolumn is able to preserve the scroll offset - how?
a
rememberLazyListState()
uses
rememberSaveable
under the hood, which allows persisting state through navigation happening
u
oh, did not know that, thanks!