Browse Source

Merge pull request #1920 from vector-im/feature/fga/dynamic_room_list_incremental_load

RoomList: introduce incremental loading to improve performances.
pull/1943/head
ganfra 10 months ago committed by GitHub
parent
commit
6cf2099b03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      changelog.d/1920.misc
  2. 46
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt
  3. 4
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt
  4. 5
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt
  5. 65
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt
  6. 17
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt
  7. 5
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt
  8. 13
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt

1
changelog.d/1920.misc

@ -0,0 +1 @@ @@ -0,0 +1 @@
RoomList: introduce incremental loading to improve performances.

46
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt

@ -16,6 +16,12 @@ @@ -16,6 +16,12 @@
package io.element.android.libraries.matrix.api.roomlist
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
/**
* RoomList with dynamic filtering and loading.
* This is useful for large lists of rooms.
@ -23,17 +29,17 @@ package io.element.android.libraries.matrix.api.roomlist @@ -23,17 +29,17 @@ package io.element.android.libraries.matrix.api.roomlist
*/
interface DynamicRoomList : RoomList {
companion object {
const val DEFAULT_PAGE_SIZE = 20
const val DEFAULT_PAGES_TO_LOAD = 10
}
sealed interface Filter {
/**
* No filter applied.
*/
data object All : Filter
/**
* Filter only the left rooms.
*/
data object AllNonLeft : Filter
/**
* Filter all rooms.
*/
@ -45,6 +51,10 @@ interface DynamicRoomList : RoomList { @@ -45,6 +51,10 @@ interface DynamicRoomList : RoomList {
data class NormalizedMatchRoomName(val pattern: String) : Filter
}
val currentFilter: StateFlow<Filter>
val loadedPages: StateFlow<Int>
val pageSize: Int
/**
* Load more rooms into the list if possible.
*/
@ -61,3 +71,29 @@ interface DynamicRoomList : RoomList { @@ -61,3 +71,29 @@ interface DynamicRoomList : RoomList {
*/
suspend fun updateFilter(filter: Filter)
}
/**
* Offers a way to load all the rooms incrementally.
* It will load more room until all are loaded.
* If total number of rooms increase, it will load more pages if needed.
* The number of rooms is independent of the filter.
*/
fun DynamicRoomList.loadAllIncrementally(coroutineScope: CoroutineScope) {
combine(
loadedPages,
loadingState,
) { loadedPages, loadingState ->
loadedPages to loadingState
}
.onEach { (loadedPages, loadingState) ->
when (loadingState) {
is RoomList.LoadingState.Loaded -> {
if (pageSize * loadedPages < loadingState.numberOfRooms) {
loadMore()
}
}
RoomList.LoadingState.NotLoaded -> Unit
}
}
.launchIn(coroutineScope)
}

4
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt

@ -41,10 +41,10 @@ interface RoomListService { @@ -41,10 +41,10 @@ interface RoomListService {
}
/**
* returns a [RoomList] object of all rooms we want to display.
* returns a [DynamicRoomList] object of all rooms we want to display.
* This will exclude some rooms like the invites, or spaces.
*/
val allRooms: RoomList
val allRooms: DynamicRoomList
/**
* returns a [RoomList] object of all invites.

5
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt

@ -65,7 +65,6 @@ fun RoomListInterface.loadingStateFlow(): Flow<RoomListLoadingState> = @@ -65,7 +65,6 @@ fun RoomListInterface.loadingStateFlow(): Flow<RoomListLoadingState> =
internal fun RoomListInterface.entriesFlow(
pageSize: Int,
numberOfPages: Int,
roomListDynamicEvents: Flow<RoomListDynamicEvents>,
initialFilterKind: RoomListEntriesDynamicFilterKind
): Flow<List<RoomListEntriesUpdate>> =
@ -84,9 +83,7 @@ internal fun RoomListInterface.entriesFlow( @@ -84,9 +83,7 @@ internal fun RoomListInterface.entriesFlow(
controller.setFilter(controllerEvents.filter)
}
is RoomListDynamicEvents.LoadMore -> {
repeat(numberOfPages) {
controller.addOnePage()
}
controller.addOnePage()
}
is RoomListDynamicEvents.Reset -> {
controller.resetToOnePage()

65
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt

@ -23,6 +23,7 @@ import kotlinx.coroutines.CoroutineDispatcher @@ -23,6 +23,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -39,57 +40,28 @@ internal class RoomListFactory( @@ -39,57 +40,28 @@ internal class RoomListFactory(
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
) {
/**
* Creates a room list that will load all rooms in a single page.
* It mimics the usage of the old api.
*/
fun createRoomList(
innerProvider: suspend () -> InnerRoomList,
): RoomList {
return createRustRoomList(
pageSize = Int.MAX_VALUE,
numberOfPages = 1,
initialFilterKind = RoomListEntriesDynamicFilterKind.AllNonLeft,
innerRoomListProvider = innerProvider
)
}
/**
* Creates a room list that can be used to load more rooms and filter them dynamically.
*/
fun createDynamicRoomList(
pageSize: Int = DynamicRoomList.DEFAULT_PAGE_SIZE,
pagesToLoad: Int = DynamicRoomList.DEFAULT_PAGES_TO_LOAD,
initialFilter: DynamicRoomList.Filter = DynamicRoomList.Filter.None,
fun createRoomList(
pageSize: Int,
initialFilter: DynamicRoomList.Filter = DynamicRoomList.Filter.All,
innerProvider: suspend () -> InnerRoomList
): DynamicRoomList {
return createRustRoomList(
pageSize = pageSize,
numberOfPages = pagesToLoad,
initialFilterKind = initialFilter.toRustFilter(),
innerRoomListProvider = innerProvider
)
}
private fun createRustRoomList(
pageSize: Int,
numberOfPages: Int,
initialFilterKind: RoomListEntriesDynamicFilterKind,
innerRoomListProvider: suspend () -> InnerRoomList
): RustDynamicRoomList {
val loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
val summariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, dispatcher, roomSummaryDetailsFactory)
val dynamicEvents = MutableSharedFlow<RoomListDynamicEvents>()
// Makes sure we don't miss any events
val dynamicEvents = MutableSharedFlow<RoomListDynamicEvents>(replay = 100)
val currentFilter = MutableStateFlow(initialFilter)
val loadedPages = MutableStateFlow(1)
var innerRoomList: InnerRoomList? = null
coroutineScope.launch(dispatcher) {
innerRoomList = innerRoomListProvider()
innerRoomList = innerProvider()
innerRoomList?.let { innerRoomList ->
innerRoomList.entriesFlow(
pageSize = pageSize,
numberOfPages = numberOfPages,
initialFilterKind = initialFilterKind,
initialFilterKind = initialFilter.toRustFilter(),
roomListDynamicEvents = dynamicEvents
).onEach { update ->
processor.postUpdate(update)
@ -105,15 +77,26 @@ internal class RoomListFactory( @@ -105,15 +77,26 @@ internal class RoomListFactory(
}.invokeOnCompletion {
innerRoomList?.destroy()
}
return RustDynamicRoomList(summariesFlow, loadingStateFlow, dynamicEvents, processor)
return RustDynamicRoomList(
summaries = summariesFlow,
loadingState = loadingStateFlow,
currentFilter = currentFilter,
loadedPages = loadedPages,
dynamicEvents = dynamicEvents,
processor = processor,
pageSize = pageSize,
)
}
}
private class RustDynamicRoomList(
override val summaries: MutableStateFlow<List<RoomSummary>>,
override val loadingState: MutableStateFlow<RoomList.LoadingState>,
override val currentFilter: MutableStateFlow<DynamicRoomList.Filter>,
override val loadedPages: MutableStateFlow<Int>,
private val dynamicEvents: MutableSharedFlow<RoomListDynamicEvents>,
private val processor: RoomSummaryListProcessor,
override val pageSize: Int,
) : DynamicRoomList {
override suspend fun rebuildSummaries() {
@ -121,16 +104,19 @@ private class RustDynamicRoomList( @@ -121,16 +104,19 @@ private class RustDynamicRoomList(
}
override suspend fun updateFilter(filter: DynamicRoomList.Filter) {
currentFilter.emit(filter)
val filterEvent = RoomListDynamicEvents.SetFilter(filter.toRustFilter())
dynamicEvents.emit(filterEvent)
}
override suspend fun loadMore() {
dynamicEvents.emit(RoomListDynamicEvents.LoadMore)
loadedPages.getAndUpdate { it + 1 }
}
override suspend fun reset() {
dynamicEvents.emit(RoomListDynamicEvents.Reset)
loadedPages.emit(1)
}
}
@ -146,6 +132,7 @@ private fun DynamicRoomList.Filter.toRustFilter(): RoomListEntriesDynamicFilterK @@ -146,6 +132,7 @@ private fun DynamicRoomList.Filter.toRustFilter(): RoomListEntriesDynamicFilterK
DynamicRoomList.Filter.All -> RoomListEntriesDynamicFilterKind.All
is DynamicRoomList.Filter.NormalizedMatchRoomName -> RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName(this.pattern)
DynamicRoomList.Filter.None -> RoomListEntriesDynamicFilterKind.None
DynamicRoomList.Filter.AllNonLeft -> RoomListEntriesDynamicFilterKind.AllNonLeft
}
}

17
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt

@ -16,8 +16,10 @@ @@ -16,8 +16,10 @@
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -34,20 +36,31 @@ import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator @@ -34,20 +36,31 @@ import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
import timber.log.Timber
import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
private const val DEFAULT_PAGE_SIZE = 20
internal class RustRoomListService(
private val innerRoomListService: InnerRustRoomListService,
private val sessionCoroutineScope: CoroutineScope,
roomListFactory: RoomListFactory,
) : RoomListService {
override val allRooms: RoomList = roomListFactory.createRoomList {
override val allRooms: DynamicRoomList = roomListFactory.createRoomList(
pageSize = DEFAULT_PAGE_SIZE,
initialFilter = DynamicRoomList.Filter.AllNonLeft,
) {
innerRoomListService.allRooms()
}
override val invites: RoomList = roomListFactory.createRoomList {
override val invites: RoomList = roomListFactory.createRoomList(
pageSize = Int.MAX_VALUE,
) {
innerRoomListService.invites()
}
init {
allRooms.loadAllIncrementally(sessionCoroutineScope)
}
override fun updateAllRoomsVisibleRange(range: IntRange) {
Timber.v("setVisibleRange=$range")
sessionCoroutineScope.launch {

5
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.test.roomlist
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
@ -54,14 +55,16 @@ class FakeRoomListService : RoomListService { @@ -54,14 +55,16 @@ class FakeRoomListService : RoomListService {
var latestSlidingSyncRange: IntRange? = null
private set
override val allRooms: RoomList = SimplePagedRoomList(
override val allRooms: DynamicRoomList = SimplePagedRoomList(
allRoomSummariesFlow,
allRoomsLoadingStateFlow,
MutableStateFlow(DynamicRoomList.Filter.None)
)
override val invites: RoomList = SimplePagedRoomList(
inviteRoomSummariesFlow,
inviteRoomsLoadingStateFlow,
MutableStateFlow(DynamicRoomList.Filter.None)
)
override fun updateAllRoomsVisibleRange(range: IntRange) {

13
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt

@ -19,23 +19,30 @@ package io.element.android.libraries.matrix.test.roomlist @@ -19,23 +19,30 @@ package io.element.android.libraries.matrix.test.roomlist
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.getAndUpdate
data class SimplePagedRoomList(
override val summaries: StateFlow<List<RoomSummary>>,
override val loadingState: StateFlow<RoomList.LoadingState>
override val loadingState: StateFlow<RoomList.LoadingState>,
override val currentFilter: MutableStateFlow<DynamicRoomList.Filter>
) : DynamicRoomList {
override val pageSize: Int = Int.MAX_VALUE
override val loadedPages = MutableStateFlow(1)
override suspend fun loadMore() {
//No-op
loadedPages.getAndUpdate { it + 1 }
}
override suspend fun reset() {
//No-op
loadedPages.emit(1)
}
override suspend fun updateFilter(filter: DynamicRoomList.Filter) {
//No-op
currentFilter.emit(filter)
}
override suspend fun rebuildSummaries() {

Loading…
Cancel
Save