Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ import io.getstream.chat.android.compose.ui.threads.ThreadItem
import io.getstream.chat.android.compose.ui.util.StreamSnackbar
import io.getstream.chat.android.compose.ui.util.bottomBorder
import io.getstream.chat.android.compose.ui.util.topBorder
import io.getstream.chat.android.compose.util.isImeAnimating
import io.getstream.chat.android.compose.viewmodel.messages.AudioPlayerViewModelFactory
import io.getstream.chat.android.models.ConnectionState
import io.getstream.chat.android.models.Message
Expand Down Expand Up @@ -857,14 +858,19 @@ public interface ChatComponentFactory {
/**
* The default message list item modifier for styling.
*
* Uses `Modifier.animateItem` for fade in/out and placement transitions. The placement
* animation is disabled while the IME is animating so items don't slide during the
* keyboard open/close window; it resumes for normal list changes (insertions, deletions,
* height changes).
*
* @param params Parameters for this component.
*/
@Composable
public fun LazyItemScope.messageListItemModifier(params: MessageListItemModifierParams): Modifier =
if (LocalInspectionMode.current) {
Modifier
} else {
Modifier.animateItem()
when {
LocalInspectionMode.current -> Modifier
isImeAnimating() -> Modifier.animateItem(placementSpec = null)
else -> Modifier.animateItem()
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.compose.util

import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.imeAnimationSource
import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity

/**
* Returns `true` while an IME animation is in progress; `false` when it's fully settled
* (either open or closed).
*
* `imeAnimationSource` and `imeAnimationTarget` diverge for the whole duration of the
* animation and converge again once it settles. Reading them synchronously in composition
* means the value flips on the same frame the animation starts, avoiding a one-frame lag
* that a coroutine-based debounce approach would incur at the transition edges.
*/
@Composable
@OptIn(ExperimentalLayoutApi::class)
internal fun isImeAnimating(): Boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The neighbouring isKeyboardVisibleAsState() avoids WindowInsets.ime because it can report wrong values on some OEM ROMs (Xiaomi/MIUI), see its KDoc. This helper reads imeAnimationSource / imeAnimationTarget from the same IME inset channel, so on those ROMs (or when the host window isn't set up for edge-to-edge inset dispatch) it may never report source != target and the fix just won't apply. It degrades gracefully, so not blocking. Worth a one-line note in the KDoc to document the dependency?

val density = LocalDensity.current
val source = WindowInsets.imeAnimationSource.getBottom(density)
val target = WindowInsets.imeAnimationTarget.getBottom(density)
return source != target
}
Comment on lines +35 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

# Locate relevant files and usages
git ls-files | rg 'stream-chat-android-compose/.*/IsImeAnimating\.kt|stream-chat-android-compose/.*/Messages|stream-chat-android-compose/.*/ChatComponentFactory|stream-chat-android-compose/.*/MessageList|stream-chat-android-compose/.*/ChannelList'
printf '\n--- isImeAnimating usages ---\n'
rg -n "isImeAnimating\(" stream-chat-android-compose || true
printf '\n--- imeAnimationSource/Target usages ---\n'
rg -n "imeAnimation(Source|Target)" stream-chat-android-compose || true

printf '\n--- target file ---\n'
nl -ba stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt | sed -n '1,200p'

Repository: GetStream/stream-chat-android

Length of output: 4619


🏁 Script executed:

set -euo pipefail

# Map the target file and likely caller sites
git ls-files | rg 'stream-chat-android-compose/.*/IsImeAnimating\.kt|stream-chat-android-compose/.*/Messages|stream-chat-android-compose/.*/ChatComponentFactory|stream-chat-android-compose/.*/MessageList|stream-chat-android-compose/.*/ChannelList'
printf '\n--- isImeAnimating usages ---\n'
rg -n "isImeAnimating\(" stream-chat-android-compose || true
printf '\n--- imeAnimationSource/Target usages ---\n'
rg -n "imeAnimation(Source|Target)" stream-chat-android-compose || true

printf '\n--- target file ---\n'
nl -ba stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt | sed -n '1,200p'

Repository: GetStream/stream-chat-android

Length of output: 4619


🏁 Script executed:

set -euo pipefail

git ls-files | rg 'stream-chat-android-compose/.*/IsImeAnimating\.kt|stream-chat-android-compose/.*/Messages|stream-chat-android-compose/.*/ChatComponentFactory|stream-chat-android-compose/.*/MessageList|stream-chat-android-compose/.*/ChannelList'
printf '\n--- isImeAnimating usages ---\n'
rg -n "isImeAnimating\(" stream-chat-android-compose || true
printf '\n--- imeAnimationSource/Target usages ---\n'
rg -n "imeAnimation(Source|Target)" stream-chat-android-compose || true

printf '\n--- target file ---\n'
nl -ba stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt | sed -n '1,200p'

Repository: GetStream/stream-chat-android

Length of output: 4619


🌐 Web query:

Jetpack Compose WindowInsets documentation should not be read during composition avoid doing composition for every frame of an animation

💡 Result:

In Jetpack Compose, WindowInsets values should not be read directly during the composition phase [1][2]. Doing so causes the composition to re-execute whenever the insets change, which—during an animation—results in triggering composition for every single frame [1][2]. This can lead to significant performance degradation and potential UI lag [1][2][3]. To avoid this, you should use built-in Compose modifiers and APIs designed to handle insets efficiently without triggering unnecessary recomposition [1][2]. Recommended practices include: Use specialized modifiers: Utilize modifiers such as Modifier.windowInsetsPadding, Modifier.systemBarsPadding, Modifier.imePadding, or Modifier.windowInsetsTopHeight [1][2]. These modifiers are designed to apply inset changes during the layout phase rather than the composition phase, ensuring performance efficiency even during animations [1][2]. Avoid reading values in composition: Do not access properties like WindowInsets.getTop(density) or WindowInsets.getBottom(density) within your Composable functions if those values change frequently [1][2]. If you have complex requirements that necessitate manual handling, look into APIs like onConsumedWindowInsetsChanged or MutableWindowInsets, though these should be used with care to maintain proper performance patterns [4][5]. Generally, sticking to the provided layout modifiers is the most performant and recommended approach [1][2].

Citations:


🏁 Script executed:

set -uo pipefail

printf '%s\n' '--- files ---'
git ls-files | rg 'stream-chat-android-compose/.*/IsImeAnimating\.kt|stream-chat-android-compose/.*/Messages|stream-chat-android-compose/.*/ChatComponentFactory|stream-chat-android-compose/.*/MessageList|stream-chat-android-compose/.*/ChannelList' || true

printf '\n%s\n' '--- isImeAnimating usages ---'
rg -n "isImeAnimating\(" stream-chat-android-compose || true

printf '\n%s\n' '--- imeAnimationSource/Target usages ---'
rg -n "imeAnimation(Source|Target)" stream-chat-android-compose || true

printf '\n%s\n' '--- target file ---'
nl -ba stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt | sed -n '1,200p'

Repository: GetStream/stream-chat-android

Length of output: 4633


🏁 Script executed:

set -euo pipefail

python3 - <<'PY'
from pathlib import Path

paths = [
    Path("stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt"),
    Path("stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt"),
]
for path in paths:
    print(f"\n--- {path} ---")
    text = path.read_text()
    for i, line in enumerate(text.splitlines(), 1):
        if path.name == "IsImeAnimating.kt" or (860 <= i <= 885):
            print(f"{i:4d}: {line}")
PY

Repository: GetStream/stream-chat-android

Length of output: 3631


🏁 Script executed:

set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path("stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt")
text = path.read_text().splitlines()
for start in [840, 860, 868, 880]:
    print(f"\n--- lines {start}-{start+25} ---")
    for i in range(start, min(start+25, len(text))+1):
        print(f"{i:4d}: {text[i-1]}")
PY

Repository: GetStream/stream-chat-android

Length of output: 5014


🏁 Script executed:

set -euo pipefail

rg -n "messageListItemModifier|animateItem\(placementSpec = null\)|isImeAnimating\(\)" stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/Messages.kt stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt

Repository: GetStream/stream-chat-android

Length of output: 759


🏁 Script executed:

set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path("stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt")
for i, line in enumerate(path.read_text().splitlines(), 1):
    print(f"{i:4d}: {line}")
PY

Repository: GetStream/stream-chat-android

Length of output: 2196


🏁 Script executed:

set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path("stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt")
lines = path.read_text().splitlines()
for i in range(850, 881):
    print(f"{i:4d}: {lines[i-1]}")
PY

Repository: GetStream/stream-chat-android

Length of output: 1549


🏁 Script executed:

set -euo pipefail

python3 - <<'PY'
from pathlib import Path

files = [
    "stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt",
    "stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt",
]
for file in files:
    path = Path(file)
    print(f"\n=== {file} ===")
    lines = path.read_text().splitlines()
    if file.endswith("ChatComponentFactory.kt"):
        for start, end in [(820, 890), (1, 120)]:
            print(f"\n--- lines {start}-{end} ---")
            for i in range(start, min(end, len(lines)) + 1):
                print(f"{i:4d}: {lines[i-1]}")
    else:
        print("\n--- full file ---")
        for i, line in enumerate(lines, 1):
            print(f"{i:4d}: {line}")
PY

Repository: GetStream/stream-chat-android

Length of output: 14306


Hoist IME animation state out of messageListItemModifier

isImeAnimating() reads WindowInsets.imeAnimationSource/imeAnimationTarget during composition, so each visible message item subscribes to IME inset updates and can recompose every frame while the keyboard animates. Move this check to a shared ancestor (or pass a cached boolean down) to avoid repeating the read per item.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/util/IsImeAnimating.kt`
around lines 35 - 42, Hoist the IME animation read out of isImeAnimating() usage
in messageListItemModifier so individual message items do not subscribe to
WindowInsets.imeAnimationSource and WindowInsets.imeAnimationTarget during
composition. Move the isImeAnimating() call to a shared ancestor composable,
cache the boolean there, and pass it down to the message item modifier or
related UI path so only one composable tracks the inset animation state.

Loading