sdk : allow passing coroutineScope to RoomList

This commit is contained in:
ganfra
2026-01-21 15:44:37 +01:00
parent 941340f250
commit edba196a69
15 changed files with 83 additions and 35 deletions

View File

@@ -8,7 +8,9 @@
package io.element.android.features.home.impl.search package io.element.android.features.home.impl.search
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.features.home.impl.datasource.RoomListRoomSummaryFactory import io.element.android.features.home.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.home.impl.model.RoomListRoomSummary import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@@ -18,6 +20,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
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.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@@ -25,16 +28,23 @@ import kotlinx.coroutines.flow.map
private const val PAGE_SIZE = 30 private const val PAGE_SIZE = 30
@Inject @AssistedInject
class RoomListSearchDataSource( class RoomListSearchDataSource(
@Assisted coroutineScope: CoroutineScope,
roomListService: RoomListService, roomListService: RoomListService,
coroutineDispatchers: CoroutineDispatchers, coroutineDispatchers: CoroutineDispatchers,
private val roomSummaryFactory: RoomListRoomSummaryFactory, private val roomSummaryFactory: RoomListRoomSummaryFactory,
) { ) {
@AssistedFactory
interface Factory {
fun create(coroutineScope: CoroutineScope): RoomListSearchDataSource
}
private val roomList = roomListService.createRoomList( private val roomList = roomListService.createRoomList(
pageSize = PAGE_SIZE, pageSize = PAGE_SIZE,
initialFilter = RoomListFilter.None, initialFilter = RoomListFilter.None,
source = RoomList.Source.All, source = RoomList.Source.All,
coroutineScope = coroutineScope
) )
val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.filteredSummaries val roomSummaries: Flow<ImmutableList<RoomListRoomSummary>> = roomList.filteredSummaries

View File

@@ -16,6 +16,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@@ -23,7 +24,7 @@ import kotlinx.collections.immutable.persistentListOf
@Inject @Inject
class RoomListSearchPresenter( class RoomListSearchPresenter(
private val dataSource: RoomListSearchDataSource, private val dataSourceFactory: RoomListSearchDataSource.Factory,
) : Presenter<RoomListSearchState> { ) : Presenter<RoomListSearchState> {
@Composable @Composable
override fun present(): RoomListSearchState { override fun present(): RoomListSearchState {
@@ -33,6 +34,9 @@ class RoomListSearchPresenter(
} }
val searchQuery = rememberTextFieldState() val searchQuery = rememberTextFieldState()
val coroutineScope = rememberCoroutineScope()
val dataSource = remember { dataSourceFactory.create(coroutineScope) }
LaunchedEffect(isSearchActive) { LaunchedEffect(isSearchActive) {
dataSource.setIsActive(isSearchActive) dataSource.setIsActive(isSearchActive)
} }

View File

@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@@ -122,13 +123,18 @@ fun TestScope.createRoomListSearchPresenter(
roomListService: RoomListService = FakeRoomListService(), roomListService: RoomListService = FakeRoomListService(),
): RoomListSearchPresenter { ): RoomListSearchPresenter {
return RoomListSearchPresenter( return RoomListSearchPresenter(
dataSource = RoomListSearchDataSource( dataSourceFactory = object : RoomListSearchDataSource.Factory {
roomListService = roomListService, override fun create(coroutineScope: CoroutineScope): RoomListSearchDataSource {
roomSummaryFactory = aRoomListRoomSummaryFactory( return RoomListSearchDataSource(
dateFormatter = FakeDateFormatter(), roomListService = roomListService,
roomLatestEventFormatter = FakeRoomLatestEventFormatter(), roomSummaryFactory = aRoomListRoomSummaryFactory(
), dateFormatter = FakeDateFormatter(),
coroutineDispatchers = testCoroutineDispatchers(), roomLatestEventFormatter = FakeRoomLatestEventFormatter(),
), ),
coroutineDispatchers = testCoroutineDispatchers(),
coroutineScope = coroutineScope,
)
}
}
) )
} }

View File

@@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.api.roomlist
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
@@ -39,11 +40,13 @@ interface RoomListService {
* @param pageSize the number of rooms to load at once. * @param pageSize the number of rooms to load at once.
* @param initialFilter the initial filter to apply to the rooms. * @param initialFilter the initial filter to apply to the rooms.
* @param source the source of the rooms, either all rooms or invites. * @param source the source of the rooms, either all rooms or invites.
* @param coroutineScope the coroutine scope to use for the room list operations.
*/ */
fun createRoomList( fun createRoomList(
pageSize: Int, pageSize: Int,
initialFilter: RoomListFilter, initialFilter: RoomListFilter,
source: RoomList.Source, source: RoomList.Source,
coroutineScope: CoroutineScope,
): DynamicRoomList ): DynamicRoomList
/** /**

View File

@@ -192,7 +192,6 @@ class RustMatrixClient(
sessionDispatcher = sessionDispatcher, sessionDispatcher = sessionDispatcher,
roomListFactory = RoomListFactory( roomListFactory = RoomListFactory(
innerRoomListService = innerRoomListService, innerRoomListService = innerRoomListService,
sessionCoroutineScope = sessionCoroutineScope,
analyticsService = analyticsService, analyticsService = analyticsService,
), ),
roomSyncSubscriber = roomSyncSubscriber, roomSyncSubscriber = roomSyncSubscriber,

View File

@@ -38,7 +38,6 @@ private val ROOM_LIST_RUST_FILTERS = listOf(
internal class RoomListFactory( internal class RoomListFactory(
private val innerRoomListService: RoomListService, private val innerRoomListService: RoomListService,
private val sessionCoroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
) { ) {
private val roomSummaryFactory: RoomSummaryFactory = RoomSummaryFactory() private val roomSummaryFactory: RoomSummaryFactory = RoomSummaryFactory()
@@ -49,7 +48,7 @@ internal class RoomListFactory(
fun createRoomList( fun createRoomList(
pageSize: Int, pageSize: Int,
coroutineContext: CoroutineContext, coroutineContext: CoroutineContext,
coroutineScope: CoroutineScope = sessionCoroutineScope, coroutineScope: CoroutineScope,
initialFilter: RoomListFilter = RoomListFilter.all(), initialFilter: RoomListFilter = RoomListFilter.all(),
innerProvider: suspend () -> InnerRoomList innerProvider: suspend () -> InnerRoomList
): DynamicRoomList { ): DynamicRoomList {

View File

@@ -35,17 +35,19 @@ internal class RustRoomListService(
private val sessionDispatcher: CoroutineDispatcher, private val sessionDispatcher: CoroutineDispatcher,
private val roomListFactory: RoomListFactory, private val roomListFactory: RoomListFactory,
private val roomSyncSubscriber: RoomSyncSubscriber, private val roomSyncSubscriber: RoomSyncSubscriber,
sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
) : RoomListService { ) : RoomListService {
override fun createRoomList( override fun createRoomList(
pageSize: Int, pageSize: Int,
initialFilter: RoomListFilter, initialFilter: RoomListFilter,
source: RoomList.Source source: RoomList.Source,
coroutineScope: CoroutineScope,
): DynamicRoomList { ): DynamicRoomList {
return roomListFactory.createRoomList( return roomListFactory.createRoomList(
pageSize = pageSize, pageSize = pageSize,
initialFilter = initialFilter, initialFilter = initialFilter,
coroutineContext = sessionDispatcher, coroutineContext = sessionDispatcher,
coroutineScope = coroutineScope,
) { ) {
when (source) { when (source) {
RoomList.Source.All -> innerRoomListService.allRooms() RoomList.Source.All -> innerRoomListService.allRooms()
@@ -60,6 +62,7 @@ internal class RustRoomListService(
override val allRooms: DynamicRoomList = roomListFactory.createRoomList( override val allRooms: DynamicRoomList = roomListFactory.createRoomList(
pageSize = DEFAULT_PAGE_SIZE, pageSize = DEFAULT_PAGE_SIZE,
coroutineContext = sessionDispatcher, coroutineContext = sessionDispatcher,
coroutineScope = sessionCoroutineScope,
) { ) {
innerRoomListService.allRooms() innerRoomListService.allRooms()
} }

View File

@@ -20,12 +20,12 @@ class RoomListFactoryTest {
fun `createRoomList should work`() = runTest { fun `createRoomList should work`() = runTest {
val sut = RoomListFactory( val sut = RoomListFactory(
innerRoomListService = FakeFfiRoomListService(), innerRoomListService = FakeFfiRoomListService(),
sessionCoroutineScope = backgroundScope,
analyticsService = FakeAnalyticsService(), analyticsService = FakeAnalyticsService(),
) )
sut.createRoomList( sut.createRoomList(
pageSize = 10, pageSize = 10,
coroutineContext = EmptyCoroutineContext, coroutineContext = EmptyCoroutineContext,
coroutineScope = backgroundScope,
) { ) {
FakeFfiRoomList() FakeFfiRoomList()
} }

View File

@@ -50,7 +50,6 @@ private fun TestScope.createRustRoomListService(
sessionDispatcher = StandardTestDispatcher(testScheduler), sessionDispatcher = StandardTestDispatcher(testScheduler),
roomListFactory = RoomListFactory( roomListFactory = RoomListFactory(
innerRoomListService = roomListService, innerRoomListService = roomListService,
sessionCoroutineScope = backgroundScope,
analyticsService = FakeAnalyticsService(), analyticsService = FakeAnalyticsService(),
), ),
roomSyncSubscriber = RoomSyncSubscriber( roomSyncSubscriber = RoomSyncSubscriber(

View File

@@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -44,7 +45,8 @@ class FakeRoomListService(
override fun createRoomList( override fun createRoomList(
pageSize: Int, pageSize: Int,
initialFilter: RoomListFilter, initialFilter: RoomListFilter,
source: RoomList.Source source: RoomList.Source,
coroutineScope: CoroutineScope,
): DynamicRoomList { ): DynamicRoomList {
return when (source) { return when (source) {
RoomList.Source.All -> allRooms RoomList.Source.All -> allRooms

View File

@@ -9,6 +9,8 @@
package io.element.android.libraries.roomselect.impl package io.element.android.libraries.roomselect.impl
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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
@@ -17,6 +19,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.appyx.launchMolecule
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.di.SessionScope import io.element.android.libraries.di.SessionScope
@@ -36,11 +39,12 @@ class RoomSelectNode(
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()
private val presenter = presenterFactory.create(inputs.mode) private val presenter = presenterFactory.create(inputs.mode)
private val stateFlow = launchMolecule { presenter.present() }
private val callback: RoomSelectEntryPoint.Callback = callback() private val callback: RoomSelectEntryPoint.Callback = callback()
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state by stateFlow.collectAsState()
RoomSelectView( RoomSelectView(
state = state, state = state,
onDismiss = callback::onCancel, onDismiss = callback::onCancel,

View File

@@ -16,6 +16,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedFactory
@@ -31,7 +32,7 @@ import kotlinx.collections.immutable.toImmutableList
@AssistedInject @AssistedInject
class RoomSelectPresenter( class RoomSelectPresenter(
@Assisted private val mode: RoomSelectMode, @Assisted private val mode: RoomSelectMode,
private val dataSource: RoomSelectSearchDataSource, private val dataSourceFactory: RoomSelectSearchDataSource.Factory,
) : Presenter<RoomSelectState> { ) : Presenter<RoomSelectState> {
@AssistedFactory @AssistedFactory
fun interface Factory { fun interface Factory {
@@ -44,9 +45,8 @@ class RoomSelectPresenter(
var searchQuery by remember { mutableStateOf("") } var searchQuery by remember { mutableStateOf("") }
var isSearchActive by remember { mutableStateOf(false) } var isSearchActive by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { val coroutineScope = rememberCoroutineScope()
dataSource.load() val dataSource = remember { dataSourceFactory.create(coroutineScope) }
}
LaunchedEffect(searchQuery) { LaunchedEffect(searchQuery) {
dataSource.setSearchQuery(searchQuery) dataSource.setSearchQuery(searchQuery)

View File

@@ -8,7 +8,9 @@
package io.element.android.libraries.roomselect.impl package io.element.android.libraries.roomselect.impl
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomList
@@ -19,6 +21,7 @@ import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
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.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@@ -30,16 +33,25 @@ private const val PAGE_SIZE = 30
* DataSource for RoomSummaryDetails that can be filtered by a search query, * DataSource for RoomSummaryDetails that can be filtered by a search query,
* and which only includes rooms the user has joined. * and which only includes rooms the user has joined.
*/ */
@Inject @AssistedInject
class RoomSelectSearchDataSource( class RoomSelectSearchDataSource(
@Assisted coroutineScope: CoroutineScope,
roomListService: RoomListService, roomListService: RoomListService,
coroutineDispatchers: CoroutineDispatchers, coroutineDispatchers: CoroutineDispatchers,
) { ) {
@AssistedFactory
interface Factory {
fun create(coroutineScope: CoroutineScope): RoomSelectSearchDataSource
}
private val roomList = roomListService.createRoomList( private val roomList = roomListService.createRoomList(
pageSize = PAGE_SIZE, pageSize = PAGE_SIZE,
initialFilter = RoomListFilter.all(), initialFilter = RoomListFilter.all(),
source = RoomList.Source.All, source = RoomList.Source.All,
) coroutineScope = coroutineScope
).apply {
loadAllIncrementally(coroutineScope)
}
val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = roomList.filteredSummaries val roomInfoList: Flow<ImmutableList<SelectRoomInfo>> = roomList.filteredSummaries
.map { roomSummaries -> .map { roomSummaries ->
@@ -51,10 +63,6 @@ class RoomSelectSearchDataSource(
} }
.flowOn(coroutineDispatchers.computation) .flowOn(coroutineDispatchers.computation)
suspend fun load() = coroutineScope {
roomList.loadAllIncrementally(this)
}
suspend fun setSearchQuery(searchQuery: String) = coroutineScope { suspend fun setSearchQuery(searchQuery: String) = coroutineScope {
val filter = if (searchQuery.isBlank()) { val filter = if (searchQuery.isBlank()) {
RoomListFilter.all() RoomListFilter.all()

View File

@@ -9,6 +9,7 @@
package io.element.android.libraries.roomselect.impl package io.element.android.libraries.roomselect.impl
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@@ -16,14 +17,18 @@ import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode import io.element.android.tests.testutils.node.TestParentNode
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DefaultRoomSelectEntryPointTest { class DefaultRoomSelectEntryPointTest {
@get:Rule @get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule() val instantTaskExecutorRule = InstantTaskExecutorRule()
@OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun `test node builder`() = runTest { fun `test node builder`() = runTest {
val entryPoint = DefaultRoomSelectEntryPoint() val entryPoint = DefaultRoomSelectEntryPoint()

View File

@@ -22,6 +22,7 @@ import io.element.android.libraries.roomselect.api.RoomSelectMode
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@@ -119,8 +120,13 @@ internal fun TestScope.createRoomSelectPresenter(
roomListService: RoomListService = FakeRoomListService(), roomListService: RoomListService = FakeRoomListService(),
) = RoomSelectPresenter( ) = RoomSelectPresenter(
mode = mode, mode = mode,
dataSource = RoomSelectSearchDataSource( dataSourceFactory = object : RoomSelectSearchDataSource.Factory {
roomListService = roomListService, override fun create(coroutineScope: CoroutineScope): RoomSelectSearchDataSource {
coroutineDispatchers = testCoroutineDispatchers(), return RoomSelectSearchDataSource(
), coroutineScope = coroutineScope,
roomListService = roomListService,
coroutineDispatchers = testCoroutineDispatchers(),
)
}
}
) )