From 1ef2e901fc67c8819527dea9ce02293499a3f143 Mon Sep 17 00:00:00 2001 From: Phillip Thelen Date: Tue, 28 Mar 2023 11:58:41 +0200 Subject: [PATCH] paginate party seeking --- Habitica/build.gradle | 1 + .../android/habitica/api/ApiService.kt | 2 +- .../android/habitica/data/ApiClient.kt | 2 +- .../android/habitica/data/SocialRepository.kt | 2 +- .../data/implementation/ApiClientImpl.kt | 4 +- .../implementation/SocialRepositoryImpl.kt | 4 +- .../social/party/PartySeekingFragment.kt | 116 ++++++++++++++---- 7 files changed, 101 insertions(+), 30 deletions(-) diff --git a/Habitica/build.gradle b/Habitica/build.gradle index 25fe2356f..8d80efeb0 100644 --- a/Habitica/build.gradle +++ b/Habitica/build.gradle @@ -108,6 +108,7 @@ dependencies { implementation "androidx.navigation:navigation-ui-ktx:$navigation_version" implementation "androidx.fragment:fragment-ktx:1.5.6" implementation "androidx.paging:paging-runtime-ktx:3.1.1" + implementation "androidx.paging:paging-compose:1.0.0-alpha18" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "com.google.accompanist:accompanist-themeadapter-material:$accompanist_version" diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt index e225a07bd..c30554774 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.kt @@ -465,5 +465,5 @@ interface ApiService { suspend fun markTaskNeedsWork(@Path("taskID") taskID: String, @Path("userID") userID: String): HabitResponse @GET("looking-for-party") - suspend fun retrievePartySeekingUsers(): HabitResponse> + suspend fun retrievePartySeekingUsers(@Query("page") page: Int): HabitResponse> } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt index 4fa50fc55..440bb8e27 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt @@ -277,5 +277,5 @@ interface ApiClient { suspend fun updateMember(memberID: String, updateData: Map): Member? suspend fun getHallMember(userId: String): Member? suspend fun markTaskNeedsWork(taskID: String, userID: String): Task? - suspend fun retrievePartySeekingUsers() : List? + suspend fun retrievePartySeekingUsers(page: Int) : List? } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt index 9715cad7f..e11bd825c 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt @@ -122,5 +122,5 @@ interface SocialRepository : BaseRepository { suspend fun blockMember(userID: String): List? fun getMember(userID: String?): Flow suspend fun updateMember(memberID: String, key: String, value: Any?): Member? - suspend fun retrievePartySeekingUsers(): List? + suspend fun retrievePartySeekingUsers(page: Int = 0): List? } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt index e8e9897a9..da4f50614 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt @@ -622,8 +622,8 @@ class ApiClientImpl( return process { apiService.markTaskNeedsWork(taskID, userID) } } - override suspend fun retrievePartySeekingUsers() : List? { - return process { apiService.retrievePartySeekingUsers() } + override suspend fun retrievePartySeekingUsers(page: Int) : List? { + return process { apiService.retrievePartySeekingUsers(page) } } override suspend fun getMember(memberId: String) = processResponse(apiService.getMember(memberId)) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt index 9bf69a378..4de455798 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt @@ -48,8 +48,8 @@ class SocialRepositoryImpl( return apiClient.updateMember(memberID, mapOf(key to value)) } - override suspend fun retrievePartySeekingUsers() : List? { - return apiClient.retrievePartySeekingUsers() + override suspend fun retrievePartySeekingUsers(page: Int) : List? { + return apiClient.retrievePartySeekingUsers(page) } override fun getGroupMembership(id: String) = localRepository.getGroupMembership(userID, id) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt index 1f3e3ecf2..a5bd42843 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -17,7 +16,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material.Divider import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Text @@ -38,6 +36,15 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.fragment.app.viewModels import androidx.lifecycle.viewModelScope +import androidx.paging.LoadState +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import androidx.paging.PagingState +import androidx.paging.cachedIn +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.items import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.data.SocialRepository import com.habitrpg.android.habitica.data.UserRepository @@ -52,10 +59,12 @@ import com.habitrpg.android.habitica.ui.views.ClassText import com.habitrpg.android.habitica.ui.views.ComposableAvatarView import com.habitrpg.android.habitica.ui.views.LoadingButton import com.habitrpg.android.habitica.ui.views.LoadingButtonState +import com.habitrpg.android.habitica.ui.views.progress.HabiticaCircularProgressView import com.habitrpg.android.habitica.ui.views.progress.HabiticaPullRefreshIndicator import com.habitrpg.common.habitica.helpers.launchCatching import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow import java.util.Locale import javax.inject.Inject @@ -66,18 +75,17 @@ class PartySeekingViewModel @Inject constructor( val socialRepository : SocialRepository ) : BaseViewModel(userRepository, userViewModel) { val isRefreshing = mutableStateOf(false) - val seekingUsers = mutableStateOf>(emptyList()) + var seekingUsers: Flow> init { - retrieveUsers() - } - - fun retrieveUsers() { - isRefreshing.value = true - viewModelScope.launchCatching { - seekingUsers.value = socialRepository.retrievePartySeekingUsers() ?: emptyList() - isRefreshing.value = false - } + seekingUsers = Pager( + config = PagingConfig( + pageSize = 30, + ), + pagingSourceFactory = { + PartySeekingPagingSource(socialRepository) + } + ).flow.cachedIn(viewModelScope) } suspend fun inviteUser(member : Member) : InviteResponse? { @@ -114,11 +122,6 @@ class PartySeekingFragment : BaseFragment() { } return view } - - override fun onStart() { - super.onStart() - viewModel.retrieveUsers() - } } @Composable @@ -216,9 +219,9 @@ fun PartySeekingView( viewModel : PartySeekingViewModel, modifier : Modifier = Modifier ) { - val users : List by viewModel.seekingUsers + val pageData = viewModel.seekingUsers.collectAsLazyPagingItems() val refreshing by viewModel.isRefreshing - val pullRefreshState = rememberPullRefreshState(refreshing, { viewModel.retrieveUsers() }) + val pullRefreshState = rememberPullRefreshState(refreshing, { pageData.refresh() }) val scope = rememberCoroutineScope() val inviteStates = remember { mutableMapOf() } @@ -248,8 +251,11 @@ fun PartySeekingView( ) } } - items(users) { - PartySeekingListItem(user = it, inviteState = inviteStates[it.id] ?: LoadingButtonState.CONTENT, modifier = Modifier.animateItemPlacement()) { member -> + items( + items = pageData + ) { + if (it == null) return@items + PartySeekingListItem(user = it, inviteState = inviteStates[it.id] ?: LoadingButtonState.CONTENT, modifier = Modifier.animateItemPlacement()) { member -> scope.launchCatching({ inviteStates[member.id] = LoadingButtonState.FAILED }) { @@ -261,9 +267,73 @@ fun PartySeekingView( LoadingButtonState.FAILED } } + } + } + + when (pageData.loadState.refresh) { + is LoadState.Error -> { + } + is LoadState.Loading -> { + item { + Column( + modifier = Modifier + .fillParentMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + + HabiticaCircularProgressView() + } + } + } + else -> {} + } + + when (pageData.loadState.append) { + is LoadState.Error -> { + } + is LoadState.Loading -> { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + contentAlignment = Alignment.Center + ) { + HabiticaCircularProgressView() + } + } + } + else -> {} } - } } - HabiticaPullRefreshIndicator(users.isEmpty(), refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) + HabiticaPullRefreshIndicator(pageData.itemCount == 0, refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) + } +} + + +class PartySeekingPagingSource( + private val repository: SocialRepository, +): PagingSource() { + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val page = params.key ?: 0 + val response = repository.retrievePartySeekingUsers(page) + + LoadResult.Page( + data = response ?: emptyList(), + prevKey = if (page == 0) null else page.minus(1), + nextKey = if (response?.isEmpty() != false) null else page.plus(1), + ) + } catch (e: Exception) { + LoadResult.Error(e) + } } }