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. 3
      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 @@
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 @@
package io.element.android.libraries.matrix.api.roomlist 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. * RoomList with dynamic filtering and loading.
* This is useful for large lists of rooms. * This is useful for large lists of rooms.
@ -23,17 +29,17 @@ package io.element.android.libraries.matrix.api.roomlist
*/ */
interface DynamicRoomList : RoomList { interface DynamicRoomList : RoomList {
companion object {
const val DEFAULT_PAGE_SIZE = 20
const val DEFAULT_PAGES_TO_LOAD = 10
}
sealed interface Filter { sealed interface Filter {
/** /**
* No filter applied. * No filter applied.
*/ */
data object All : Filter data object All : Filter
/**
* Filter only the left rooms.
*/
data object AllNonLeft : Filter
/** /**
* Filter all rooms. * Filter all rooms.
*/ */
@ -45,6 +51,10 @@ interface DynamicRoomList : RoomList {
data class NormalizedMatchRoomName(val pattern: String) : Filter 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. * Load more rooms into the list if possible.
*/ */
@ -61,3 +71,29 @@ interface DynamicRoomList : RoomList {
*/ */
suspend fun updateFilter(filter: Filter) 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 {
} }
/** /**
* 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. * This will exclude some rooms like the invites, or spaces.
*/ */
val allRooms: RoomList val allRooms: DynamicRoomList
/** /**
* returns a [RoomList] object of all invites. * returns a [RoomList] object of all invites.

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

@ -65,7 +65,6 @@ fun RoomListInterface.loadingStateFlow(): Flow<RoomListLoadingState> =
internal fun RoomListInterface.entriesFlow( internal fun RoomListInterface.entriesFlow(
pageSize: Int, pageSize: Int,
numberOfPages: Int,
roomListDynamicEvents: Flow<RoomListDynamicEvents>, roomListDynamicEvents: Flow<RoomListDynamicEvents>,
initialFilterKind: RoomListEntriesDynamicFilterKind initialFilterKind: RoomListEntriesDynamicFilterKind
): Flow<List<RoomListEntriesUpdate>> = ): Flow<List<RoomListEntriesUpdate>> =
@ -84,10 +83,8 @@ internal fun RoomListInterface.entriesFlow(
controller.setFilter(controllerEvents.filter) controller.setFilter(controllerEvents.filter)
} }
is RoomListDynamicEvents.LoadMore -> { is RoomListDynamicEvents.LoadMore -> {
repeat(numberOfPages) {
controller.addOnePage() controller.addOnePage()
} }
}
is RoomListDynamicEvents.Reset -> { is RoomListDynamicEvents.Reset -> {
controller.resetToOnePage() 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
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -39,57 +40,28 @@ internal class RoomListFactory(
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), 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. * Creates a room list that can be used to load more rooms and filter them dynamically.
*/ */
fun createDynamicRoomList( fun createRoomList(
pageSize: Int = DynamicRoomList.DEFAULT_PAGE_SIZE, pageSize: Int,
pagesToLoad: Int = DynamicRoomList.DEFAULT_PAGES_TO_LOAD, initialFilter: DynamicRoomList.Filter = DynamicRoomList.Filter.All,
initialFilter: DynamicRoomList.Filter = DynamicRoomList.Filter.None,
innerProvider: suspend () -> InnerRoomList innerProvider: suspend () -> InnerRoomList
): DynamicRoomList { ): 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 loadingStateFlow: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
val summariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList()) val summariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, dispatcher, roomSummaryDetailsFactory) 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 var innerRoomList: InnerRoomList? = null
coroutineScope.launch(dispatcher) { coroutineScope.launch(dispatcher) {
innerRoomList = innerRoomListProvider() innerRoomList = innerProvider()
innerRoomList?.let { innerRoomList -> innerRoomList?.let { innerRoomList ->
innerRoomList.entriesFlow( innerRoomList.entriesFlow(
pageSize = pageSize, pageSize = pageSize,
numberOfPages = numberOfPages, initialFilterKind = initialFilter.toRustFilter(),
initialFilterKind = initialFilterKind,
roomListDynamicEvents = dynamicEvents roomListDynamicEvents = dynamicEvents
).onEach { update -> ).onEach { update ->
processor.postUpdate(update) processor.postUpdate(update)
@ -105,15 +77,26 @@ internal class RoomListFactory(
}.invokeOnCompletion { }.invokeOnCompletion {
innerRoomList?.destroy() 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( private class RustDynamicRoomList(
override val summaries: MutableStateFlow<List<RoomSummary>>, override val summaries: MutableStateFlow<List<RoomSummary>>,
override val loadingState: MutableStateFlow<RoomList.LoadingState>, override val loadingState: MutableStateFlow<RoomList.LoadingState>,
override val currentFilter: MutableStateFlow<DynamicRoomList.Filter>,
override val loadedPages: MutableStateFlow<Int>,
private val dynamicEvents: MutableSharedFlow<RoomListDynamicEvents>, private val dynamicEvents: MutableSharedFlow<RoomListDynamicEvents>,
private val processor: RoomSummaryListProcessor, private val processor: RoomSummaryListProcessor,
override val pageSize: Int,
) : DynamicRoomList { ) : DynamicRoomList {
override suspend fun rebuildSummaries() { override suspend fun rebuildSummaries() {
@ -121,16 +104,19 @@ private class RustDynamicRoomList(
} }
override suspend fun updateFilter(filter: DynamicRoomList.Filter) { override suspend fun updateFilter(filter: DynamicRoomList.Filter) {
currentFilter.emit(filter)
val filterEvent = RoomListDynamicEvents.SetFilter(filter.toRustFilter()) val filterEvent = RoomListDynamicEvents.SetFilter(filter.toRustFilter())
dynamicEvents.emit(filterEvent) dynamicEvents.emit(filterEvent)
} }
override suspend fun loadMore() { override suspend fun loadMore() {
dynamicEvents.emit(RoomListDynamicEvents.LoadMore) dynamicEvents.emit(RoomListDynamicEvents.LoadMore)
loadedPages.getAndUpdate { it + 1 }
} }
override suspend fun reset() { override suspend fun reset() {
dynamicEvents.emit(RoomListDynamicEvents.Reset) dynamicEvents.emit(RoomListDynamicEvents.Reset)
loadedPages.emit(1)
} }
} }
@ -146,6 +132,7 @@ private fun DynamicRoomList.Filter.toRustFilter(): RoomListEntriesDynamicFilterK
DynamicRoomList.Filter.All -> RoomListEntriesDynamicFilterKind.All DynamicRoomList.Filter.All -> RoomListEntriesDynamicFilterKind.All
is DynamicRoomList.Filter.NormalizedMatchRoomName -> RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName(this.pattern) is DynamicRoomList.Filter.NormalizedMatchRoomName -> RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName(this.pattern)
DynamicRoomList.Filter.None -> RoomListEntriesDynamicFilterKind.None 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 @@
package io.element.android.libraries.matrix.impl.roomlist 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.RoomList
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.loadAllIncrementally
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -34,20 +36,31 @@ import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
import timber.log.Timber import timber.log.Timber
import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
private const val DEFAULT_PAGE_SIZE = 20
internal class RustRoomListService( internal class RustRoomListService(
private val innerRoomListService: InnerRustRoomListService, private val innerRoomListService: InnerRustRoomListService,
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
roomListFactory: RoomListFactory, roomListFactory: RoomListFactory,
) : RoomListService { ) : RoomListService {
override val allRooms: RoomList = roomListFactory.createRoomList { override val allRooms: DynamicRoomList = roomListFactory.createRoomList(
pageSize = DEFAULT_PAGE_SIZE,
initialFilter = DynamicRoomList.Filter.AllNonLeft,
) {
innerRoomListService.allRooms() innerRoomListService.allRooms()
} }
override val invites: RoomList = roomListFactory.createRoomList { override val invites: RoomList = roomListFactory.createRoomList(
pageSize = Int.MAX_VALUE,
) {
innerRoomListService.invites() innerRoomListService.invites()
} }
init {
allRooms.loadAllIncrementally(sessionCoroutineScope)
}
override fun updateAllRoomsVisibleRange(range: IntRange) { override fun updateAllRoomsVisibleRange(range: IntRange) {
Timber.v("setVisibleRange=$range") Timber.v("setVisibleRange=$range")
sessionCoroutineScope.launch { sessionCoroutineScope.launch {

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

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.test.roomlist 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.RoomList
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
@ -54,14 +55,16 @@ class FakeRoomListService : RoomListService {
var latestSlidingSyncRange: IntRange? = null var latestSlidingSyncRange: IntRange? = null
private set private set
override val allRooms: RoomList = SimplePagedRoomList( override val allRooms: DynamicRoomList = SimplePagedRoomList(
allRoomSummariesFlow, allRoomSummariesFlow,
allRoomsLoadingStateFlow, allRoomsLoadingStateFlow,
MutableStateFlow(DynamicRoomList.Filter.None)
) )
override val invites: RoomList = SimplePagedRoomList( override val invites: RoomList = SimplePagedRoomList(
inviteRoomSummariesFlow, inviteRoomSummariesFlow,
inviteRoomsLoadingStateFlow, inviteRoomsLoadingStateFlow,
MutableStateFlow(DynamicRoomList.Filter.None)
) )
override fun updateAllRoomsVisibleRange(range: IntRange) { 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
import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList 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.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.getAndUpdate
data class SimplePagedRoomList( data class SimplePagedRoomList(
override val summaries: StateFlow<List<RoomSummary>>, 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 { ) : DynamicRoomList {
override val pageSize: Int = Int.MAX_VALUE
override val loadedPages = MutableStateFlow(1)
override suspend fun loadMore() { override suspend fun loadMore() {
//No-op //No-op
loadedPages.getAndUpdate { it + 1 }
} }
override suspend fun reset() { override suspend fun reset() {
//No-op loadedPages.emit(1)
} }
override suspend fun updateFilter(filter: DynamicRoomList.Filter) { override suspend fun updateFilter(filter: DynamicRoomList.Filter) {
//No-op currentFilter.emit(filter)
} }
override suspend fun rebuildSummaries() { override suspend fun rebuildSummaries() {

Loading…
Cancel
Save