Skip to content

Conversation

@VelikovPetar
Copy link
Contributor

@VelikovPetar VelikovPetar commented Dec 5, 2025

🎯 Goal

There is threading issue with the Compose SDK that causes compose instrumented tests to fail with:

IllegalStateException: Method removeObserver must be called on the main thread
at androidx.lifecycle.LifecycleRegistry.enforceMainThreadIfNeeded(LifecycleRegistry.jvm.kt:297)
at io.getstream.chat.android.compose.util.IsAppInForegroundAsStateKt$isAppInForegroundAsState$1$1.invokeSuspend$lambda$1(IsAppInForegroundAsState.kt:42)

The cause is that the isAppInForegroundAsState() composable adds a lifecycle observer, but when the composable is disposed during test cleanup, the awaitDispose block runs on the Compose test dispatcher thread (not the main thread), causing the violation.

🛠 Implementation details

  • Ensure the removeObserver is invoked on the main thread

🎨 UI Changes

NA

🧪 Testing

  1. Apply the given patch (adds an instrumentation test which opens and closes a channel)
  2. Run the added ComposeNavigationTest and ensure it is successful
  3. If you run the same test before the changes from this PR, the test will crash with the same exception

Note: I didn't add the test as part of this PR because it is just a quickly hacked test to verify the behaviour. We need a better setup before we add such tests to the project.

Provide the patch summary here
Subject: [PATCH] Update CHANGELOG.md.
---
Index: stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/compose/robot/ComposeMessagesRobot.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/compose/robot/ComposeMessagesRobot.kt b/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/compose/robot/ComposeMessagesRobot.kt
--- a/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/compose/robot/ComposeMessagesRobot.kt	(revision f39a877c7ae3232ef468305fee398ddf48baffde)
+++ b/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/compose/robot/ComposeMessagesRobot.kt	(date 1764929065556)
@@ -20,6 +20,7 @@
 import androidx.compose.ui.test.junit4.ComposeTestRule
 import androidx.compose.ui.test.onAllNodesWithContentDescription
 import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performClick
 import androidx.compose.ui.test.performTextInput
 import org.junit.rules.TestRule
@@ -62,6 +63,27 @@
             .performClick()
     }
 
+    /**
+     * Clicks the back button to navigate back from the messages screen.
+     */
+    fun clickBackButton() {
+        composeTestRule
+            .onNodeWithTag("Stream_BackButton")
+            .performClick()
+    }
+
+    /**
+     * Waits for the messages screen to be displayed by checking for the message input.
+     */
+    fun waitForMessagesScreen() {
+        composeTestRule.waitUntil(DEFAULT_WAIT_TIMEOUT) {
+            composeTestRule
+                .onAllNodesWithContentDescription("Message input")
+                .fetchSemanticsNodes()
+                .isNotEmpty()
+        }
+    }
+
     /**
      * Assert that any message is displayed.
      */
Index: stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/compose/test/ComposeNavigationTest.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/compose/test/ComposeNavigationTest.kt b/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/compose/test/ComposeNavigationTest.kt
new file mode 100644
--- /dev/null	(date 1764929252074)
+++ b/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/compose/test/ComposeNavigationTest.kt	(date 1764929252074)
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2014-2022 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.uitests.ui.compose.test
+
+import io.getstream.chat.android.uitests.ui.BaseUiTest
+import io.getstream.chat.android.uitests.ui.compose.robot.composeChannelsRobot
+import io.getstream.chat.android.uitests.ui.compose.robot.composeLoginRobot
+import io.getstream.chat.android.uitests.ui.compose.robot.composeMessagesRobot
+import org.junit.Test
+
+/**
+ * Tests navigation between ChannelsScreen and MessagesScreen,
+ * specifically covering the scenario where MessagesScreen is disposed from composition.
+ */
+internal class ComposeNavigationTest : BaseUiTest() {
+
+    /**
+     * Tests the flow of:
+     * 1. Opening ChannelsScreen
+     * 2. Navigating to MessagesScreen
+     * 3. Closing MessagesScreen (disposing it from composition)
+     * 4. Returning back to ChannelsScreen
+     *
+     * This test ensures that MessagesScreen is properly disposed and
+     * ChannelsScreen remains functional after returning.
+     */
+    @Test
+    fun navigationTest() {
+        with(composeTestRule) {
+            composeLoginRobot {
+                selectComposeSdk()
+                loginWithUser("Petar Velikov")
+            }
+
+            composeChannelsRobot {
+                assertChannelIsDisplayed()
+                clickChannelItem()
+            }
+
+            composeMessagesRobot {
+                waitForMessagesScreen()
+                clickBackButton()
+            }
+
+            composeChannelsRobot {
+                assertChannelIsDisplayed()
+            }
+        }
+    }
+}
Index: stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/snapshot/uicomponents/channels/ChannelListItemViewTest.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/snapshot/uicomponents/channels/ChannelListItemViewTest.kt b/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/snapshot/uicomponents/channels/ChannelListItemViewTest.kt
--- a/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/snapshot/uicomponents/channels/ChannelListItemViewTest.kt	(revision f39a877c7ae3232ef468305fee398ddf48baffde)
+++ b/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/snapshot/uicomponents/channels/ChannelListItemViewTest.kt	(date 1764929098928)
@@ -58,7 +58,6 @@
                     TestData.message2(),
                 ),
                 unreadCount = 2,
-                channelLastMessageAt = TestData.date2(),
             ),
         )
     }
@@ -76,7 +75,6 @@
                         TestData.message1(),
                         TestData.message2(),
                     ),
-                    channelLastMessageAt = TestData.date2(),
                     extraData = it.extraData + mapOf("mutedChannel" to true),
                     // extraData = extraData["mutedChannel"] = true,
                 )
Index: stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ChatHelper.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ChatHelper.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ChatHelper.kt
--- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ChatHelper.kt	(revision f39a877c7ae3232ef468305fee398ddf48baffde)
+++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ChatHelper.kt	(date 1764929065535)
@@ -58,6 +58,7 @@
                 ),
             ),
             autoTranslationEnabled = ChatApp.autoTranslationEnabled,
+            requestPermissionOnAppLaunch = { false },
         )
         val notificationHandler = NotificationHandlerFactory.createNotificationHandler(
             context = context,
Index: stream-chat-android-ui-uitests/src/main/java/io/getstream/chat/android/uitests/app/login/UserCredentials.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/stream-chat-android-ui-uitests/src/main/java/io/getstream/chat/android/uitests/app/login/UserCredentials.kt b/stream-chat-android-ui-uitests/src/main/java/io/getstream/chat/android/uitests/app/login/UserCredentials.kt
--- a/stream-chat-android-ui-uitests/src/main/java/io/getstream/chat/android/uitests/app/login/UserCredentials.kt	(revision f39a877c7ae3232ef468305fee398ddf48baffde)
+++ b/stream-chat-android-ui-uitests/src/main/java/io/getstream/chat/android/uitests/app/login/UserCredentials.kt	(date 1764929173685)
@@ -24,11 +24,12 @@
 val userCredentialsList: List<UserCredentials> = listOf(
     UserCredentials(
         user = User(
-            id = "jc",
-            name = "Jc Miñarro",
-            image = "https://ca.slack-edge.com/T02RM6X6B-U011KEXDPB2-891dbb8df64f-128",
+            id = "pvelikov",
+            name = "Petar Velikov",
+            image = "https://ca.slack-edge.com/T02RM6X6B-U07LDJZRUTG-a4129fed05b6-512",
         ),
-        token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiamMifQ.WtWn2rgZyJNOpg48xUgYoMnG3BvMD5524RND7e7Mmhk",
+        token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicHZlbGlrb3YifQ." +
+            "d5eenuTIZD5gZh7rHiv3lYbE8uOqUiHfwULtUr8a-l0",
     ),
 
     UserCredentials(
Index: stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/BaseUiTest.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/BaseUiTest.kt b/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/BaseUiTest.kt
--- a/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/BaseUiTest.kt	(revision f39a877c7ae3232ef468305fee398ddf48baffde)
+++ b/stream-chat-android-ui-uitests/src/androidTest/java/io/getstream/chat/android/uitests/ui/BaseUiTest.kt	(date 1764929091552)
@@ -56,7 +56,6 @@
     @Before
     fun setup() {
         setupStreamSdk()
-        setupMockWebServer()
     }
 
     @After
@@ -73,8 +72,7 @@
             appContext = context,
         )
 
-        ChatClient.Builder("hrwwzsgrzapv", context)
-            .baseUrl(mockWebServer.url("/").toString())
+        ChatClient.Builder("qx5us2v6xvmh", context)
             .withPlugins(statePluginFactory)
             .logLevel(ChatLogLevel.ALL)
             .build()

@VelikovPetar VelikovPetar changed the title Ensure isAppInForegroundAsState lifecycle observer removal occurs on the main thread Ensure isAppInForegroundAsState lifecycle observer removal occurs on the main thread Dec 5, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Dec 5, 2025

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.25 MB 5.25 MB 0.00 MB 🟢
stream-chat-android-offline 5.48 MB 5.48 MB 0.00 MB 🟢
stream-chat-android-ui-components 10.59 MB 10.59 MB 0.00 MB 🟢
stream-chat-android-compose 12.81 MB 12.81 MB 0.00 MB 🟢

@VelikovPetar VelikovPetar marked this pull request as ready for review December 5, 2025 10:38
@VelikovPetar VelikovPetar requested a review from a team as a code owner December 5, 2025 10:38
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
61.5% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@VelikovPetar VelikovPetar merged commit 8e39f76 into develop Dec 12, 2025
12 of 13 checks passed
@VelikovPetar VelikovPetar deleted the bug/AND-943_ensure_is_app_in_foreground_as_state_is_cleaned_up_on_main branch December 12, 2025 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants