Remove runBlocking in ThreadedMessagesNode (#6108)

* Remove `runBlocking` in `ThreadedMessagesNode`. This should help reducing the number of reported ANRs
This commit is contained in:
Jorge Martin Espinosa
2026-01-30 10:16:25 +01:00
committed by GitHub
parent 63f24f0ae1
commit 585ab160ec

View File

@@ -19,6 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
@@ -29,6 +30,7 @@ import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.MessagesNavigator
import io.element.android.features.messages.impl.MessagesPresenter import io.element.android.features.messages.impl.MessagesPresenter
import io.element.android.features.messages.impl.MessagesState
import io.element.android.features.messages.impl.MessagesView import io.element.android.features.messages.impl.MessagesView
import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
@@ -44,11 +46,11 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.androidutils.system.openUrlInExternalApp
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@@ -67,22 +69,19 @@ import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ContributesNode(RoomScope::class) @ContributesNode(RoomScope::class)
@AssistedInject @AssistedInject
class ThreadedMessagesNode( class ThreadedMessagesNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val room: JoinedRoom, private val room: JoinedRoom,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
messageComposerPresenterFactory: MessageComposerPresenter.Factory, private val messageComposerPresenterFactory: MessageComposerPresenter.Factory,
timelinePresenterFactory: TimelinePresenter.Factory, private val timelinePresenterFactory: TimelinePresenter.Factory,
presenterFactory: MessagesPresenter.Factory, private val presenterFactory: MessagesPresenter.Factory,
actionListPresenterFactory: ActionListPresenter.Factory, private val actionListPresenterFactory: ActionListPresenter.Factory,
private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
private val mediaPlayer: MediaPlayer, private val mediaPlayer: MediaPlayer,
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
@@ -96,20 +95,29 @@ class ThreadedMessagesNode(
private val inputs = inputs<Inputs>() private val inputs = inputs<Inputs>()
private val callback: Callback = callback() private val callback: Callback = callback()
// TODO use a loading state node to preload this instead of using `runBlocking` private var timelineController: TimelineController? by mutableStateOf(null)
private val threadedTimeline = runBlocking { room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = inputs.threadRootEventId)).getOrThrow() } private var presenter: Presenter<MessagesState>? by mutableStateOf(null)
private val timelineController = TimelineController(room, threadedTimeline)
private val presenter = presenterFactory.create( /**
navigator = this, * This should be fast to load, but not faster than several UI frames, which will cause ANRs.
composerPresenter = messageComposerPresenterFactory.create(timelineController, this), * We'll load the [presenter] in an async way to prevent this.
timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this), */
// TODO add special processor for threaded timeline private suspend fun createPresenter(): Presenter<MessagesState> {
actionListPresenter = actionListPresenterFactory.create( val threadedTimeline = room.createTimeline(CreateTimelineParams.Threaded(threadRootEventId = inputs.threadRootEventId)).getOrThrow()
postProcessor = TimelineItemActionPostProcessor.Default, val timelineController = TimelineController(room, threadedTimeline)
timelineMode = timelineController.mainTimelineMode(), this.timelineController = timelineController
), return presenterFactory.create(
timelineController = timelineController, navigator = this,
) composerPresenter = messageComposerPresenterFactory.create(timelineController, this),
timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this),
// TODO add special processor for threaded timeline
actionListPresenter = actionListPresenterFactory.create(
postProcessor = TimelineItemActionPostProcessor.Default,
timelineMode = timelineController.mainTimelineMode(),
),
timelineController = timelineController,
)
}
interface Callback : Plugin { interface Callback : Plugin {
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
@@ -130,7 +138,10 @@ class ThreadedMessagesNode(
super.onBuilt() super.onBuilt()
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { onCreate = {
sessionCoroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) } analyticsService.capture(room.toAnalyticsViewRoom())
lifecycleScope.launch {
presenter = createPresenter()
}
}, },
onStart = { onStart = {
appNavigationStateService.onNavigateToThread(id, inputs.threadRootEventId) appNavigationStateService.onNavigateToThread(id, inputs.threadRootEventId)
@@ -231,56 +242,61 @@ class ThreadedMessagesNode(
CompositionLocalProvider( CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
) { ) {
val state = presenter.present() // Only display the actual UI and lifecycle logic if the presenter is loaded
OnLifecycleEvent { _, event -> presenter?.present()?.let { state ->
when (event) { OnLifecycleEvent { _, event ->
Lifecycle.Event.ON_PAUSE -> state.composerState.eventSink(MessageComposerEvent.SaveDraft) when (event) {
else -> Unit Lifecycle.Event.ON_PAUSE -> state.composerState.eventSink(MessageComposerEvent.SaveDraft)
} else -> Unit
}
MessagesView(
state = state,
onBackClick = this::navigateUp,
onRoomDetailsClick = {},
onEventContentClick = { isLive, event ->
if (isLive) {
callback.handleEventClick(timelineController.mainTimelineMode(), event)
} else {
val detachedTimelineMode = timelineController.detachedTimelineMode()
if (detachedTimelineMode != null) {
callback.handleEventClick(detachedTimelineMode, event)
} else {
false
}
} }
},
onUserDataClick = callback::navigateToRoomMemberDetails,
onLinkClick = { url, customTab ->
onLinkClick(
activity = activity,
darkTheme = isDark,
url = url,
eventSink = state.timelineState.eventSink,
customTab = customTab,
)
},
onSendLocationClick = callback::navigateToSendLocation,
onCreatePollClick = callback::navigateToCreatePoll,
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) },
onViewAllPinnedMessagesClick = {},
modifier = modifier,
knockRequestsBannerView = {},
)
var focusedEventId by rememberSaveable {
mutableStateOf(inputs.focusedEventId)
}
LaunchedEffect(Unit) {
focusedEventId?.also { eventId ->
state.timelineState.eventSink(TimelineEvent.FocusOnEvent(eventId))
} }
// Reset the focused event id to null to avoid refocusing when restoring node.
focusedEventId = null MessagesView(
state = state,
onBackClick = this::navigateUp,
onRoomDetailsClick = {},
onEventContentClick = { isLive, event ->
timelineController?.let { controller ->
if (isLive) {
callback.handleEventClick(controller.mainTimelineMode(), event)
} else {
val detachedTimelineMode = controller.detachedTimelineMode()
if (detachedTimelineMode != null) {
callback.handleEventClick(detachedTimelineMode, event)
} else {
false
}
}
} == true
},
onUserDataClick = callback::navigateToRoomMemberDetails,
onLinkClick = { url, customTab ->
onLinkClick(
activity = activity,
darkTheme = isDark,
url = url,
eventSink = state.timelineState.eventSink,
customTab = customTab,
)
},
onSendLocationClick = callback::navigateToSendLocation,
onCreatePollClick = callback::navigateToCreatePoll,
onJoinCallClick = { callback.navigateToRoomCall(room.roomId) },
onViewAllPinnedMessagesClick = {},
modifier = modifier,
knockRequestsBannerView = {},
)
var focusedEventId by rememberSaveable {
mutableStateOf(inputs.focusedEventId)
}
LaunchedEffect(Unit) {
focusedEventId?.also { eventId ->
state.timelineState.eventSink(TimelineEvent.FocusOnEvent(eventId))
}
// Reset the focused event id to null to avoid refocusing when restoring node.
focusedEventId = null
}
} }
} }
} }