From ed9d8b638ef32c6f75804776861ca3c3be4e549a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Fri, 24 Apr 2026 11:18:35 +0200 Subject: [PATCH] refactor: collect conversations and messages only when resumed --- .../home/conversations/ConversationScreen.kt | 14 ++--- .../ConversationsScreenContent.kt | 16 +++--- core/ui-common/build.gradle.kts | 3 + .../android/util/ui/LazyPagingItemsUtil.kt | 55 +++++++++++++++++++ 4 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 core/ui-common/src/main/kotlin/com/wire/android/util/ui/LazyPagingItemsUtil.kt diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 44c77283e40..507195c12a8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -24,6 +24,7 @@ import android.content.Context import android.net.Uri import android.text.format.DateUtils import androidx.activity.compose.BackHandler +import androidx.annotation.VisibleForTesting import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut @@ -58,10 +59,10 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -77,13 +78,11 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.annotation.VisibleForTesting import androidx.compose.ui.text.style.TextOverflow import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemContentType import androidx.paging.compose.itemKey import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination @@ -99,11 +98,13 @@ import com.ramcosta.composedestinations.result.NavResult.Value import com.ramcosta.composedestinations.result.OpenResultRecipient import com.ramcosta.composedestinations.result.ResultBackNavigator import com.ramcosta.composedestinations.result.ResultRecipient +import com.sebaslogen.resaca.rememberKeysInScope import com.wire.android.BuildConfig.IS_BUBBLE_UI_ENABLED import com.wire.android.R import com.wire.android.appLogger import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.feature.analytics.model.AnalyticsEvent +import com.wire.android.feature.cells.ui.dialog.IncompatibleFileNameDialog import com.wire.android.feature.sketch.model.DrawingCanvasNavArgs import com.wire.android.feature.sketch.model.DrawingCanvasNavBackArgs import com.wire.android.mapper.MessageDateTimeGroup @@ -145,9 +146,8 @@ import com.wire.android.ui.emoji.EmojiPickerBottomSheet import com.wire.android.ui.home.conversations.AuthorHeaderHelper.rememberShouldHaveSmallBottomPadding import com.wire.android.ui.home.conversations.AuthorHeaderHelper.rememberShouldShowHeader import com.wire.android.ui.home.conversations.ConversationSnackbarMessages.OnFileDownloaded -import com.wire.android.ui.home.conversations.attachment.MessageAttachmentsViewModel import com.wire.android.ui.home.conversations.attachment.IncompatibleFileNameDialogState -import com.wire.android.feature.cells.ui.dialog.IncompatibleFileNameDialog +import com.wire.android.ui.home.conversations.attachment.MessageAttachmentsViewModel import com.wire.android.ui.home.conversations.banner.ConversationBanner import com.wire.android.ui.home.conversations.banner.ConversationBannerViewModel import com.wire.android.ui.home.conversations.call.ConversationCallViewActions @@ -197,6 +197,7 @@ import com.wire.android.util.openDownloadFolder import com.wire.android.util.serverDate import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.UIText +import com.wire.android.util.ui.collectAsLazyPagingItemsWithLifecycle import com.wire.kalium.common.error.NetworkFailure import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode @@ -208,7 +209,6 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.isInternal import com.wire.kalium.logic.data.user.type.isTeamAdmin import com.wire.kalium.logic.feature.call.usecase.ConferenceCallingResult -import com.sebaslogen.resaca.rememberKeysInScope import kotlinx.collections.immutable.PersistentMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -1150,7 +1150,7 @@ private fun ConversationScreenContent( isBubbleUiEnabled: Boolean = false, isWireCellsEnabled: Boolean = false, ) { - val lazyPagingMessages = messages.collectAsLazyPagingItems() + val lazyPagingMessages = messages.collectAsLazyPagingItemsWithLifecycle() val lazyListState = rememberSaveable(unreadEventCount, lazyPagingMessages, saver = LazyListState.Saver) { LazyListState(unreadEventCount) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt index f24b46073a3..1cabbc64db2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt @@ -31,7 +31,12 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems +import com.ramcosta.composedestinations.generated.app.destinations.BrowseChannelsScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.ConversationFoldersScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.DebugConversationScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.NewConversationSearchPeopleScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.OtherUserProfileScreenDestination import com.wire.android.R import com.wire.android.appLogger import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl @@ -52,12 +57,6 @@ import com.wire.android.ui.common.search.SearchBarState import com.wire.android.ui.common.search.rememberSearchbarState import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.debug.conversation.DebugConversationScreenNavArgs -import com.ramcosta.composedestinations.generated.app.destinations.BrowseChannelsScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.ConversationFoldersScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.DebugConversationScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.NewConversationSearchPeopleScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.OtherUserProfileScreenDestination import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState import com.wire.android.ui.home.conversationslist.common.ConversationList import com.wire.android.ui.home.conversationslist.model.ConversationItem @@ -67,6 +66,7 @@ import com.wire.android.ui.home.conversationslist.search.SearchConversationsEmpt import com.wire.android.ui.theme.WireTheme import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.android.util.ui.SnackBarMessageHandler +import com.wire.android.util.ui.collectAsLazyPagingItemsWithLifecycle import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId @@ -164,7 +164,7 @@ fun ConversationsScreenContent( when (val state = conversationListViewModel.conversationListState) { is ConversationListState.Paginated -> { - val lazyPagingItems = state.conversations.collectAsLazyPagingItems() + val lazyPagingItems = state.conversations.collectAsLazyPagingItemsWithLifecycle() searchBarState.searchVisibleChanged(lazyPagingItems.itemCount > 0 || searchBarState.isSearchActive) when { // when conversation list is not yet fetched, show loading indicator diff --git a/core/ui-common/build.gradle.kts b/core/ui-common/build.gradle.kts index 33aee9af43c..50762a6c69c 100644 --- a/core/ui-common/build.gradle.kts +++ b/core/ui-common/build.gradle.kts @@ -32,6 +32,9 @@ dependencies { implementation(libs.visibilityModifiers) + implementation(libs.androidx.paging3) + implementation(libs.androidx.paging3Compose) + // hilt implementation(libs.hilt.navigationCompose) implementation(libs.hilt.work) diff --git a/core/ui-common/src/main/kotlin/com/wire/android/util/ui/LazyPagingItemsUtil.kt b/core/ui-common/src/main/kotlin/com/wire/android/util/ui/LazyPagingItemsUtil.kt new file mode 100644 index 00000000000..819884e37d5 --- /dev/null +++ b/core/ui-common/src/main/kotlin/com/wire/android/util/ui/LazyPagingItemsUtil.kt @@ -0,0 +1,55 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.util.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.flowWithLifecycle +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import kotlinx.coroutines.flow.Flow +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * Collects a [Flow] of [PagingData] as [LazyPagingItems] that are lifecycle-aware. + * + * This function ensures that the collection of the paging data is tied to the lifecycle of the + * composable, preventing memory leaks and unnecessary work when the composable is not active. + * + * @param minActiveState The minimum lifecycle state at which the flow should be collected. Default is [Lifecycle.State.RESUMED]. + * @param context The coroutine context to use for collecting the flow. Default is [EmptyCoroutineContext]. + * @param lifecycle The lifecycle to use for determining when to collect the flow. Default is the current one from [LocalLifecycleOwner]. + * @return A [LazyPagingItems] instance that can be used in a LazyColumn or similar composable. + */ +@Composable +fun Flow>.collectAsLazyPagingItemsWithLifecycle( + minActiveState: Lifecycle.State = Lifecycle.State.RESUMED, + context: CoroutineContext = EmptyCoroutineContext, + lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle +): LazyPagingItems { + val isPreview = LocalInspectionMode.current + val lifecycleAwareFlow = remember(this, lifecycle, isPreview) { + if (isPreview) this else this.flowWithLifecycle(lifecycle, minActiveState) + } + return lifecycleAwareFlow.collectAsLazyPagingItems(context) +}