Skip to content

Commit

Permalink
MBL-1473: Add infinite scroll to PPO screen (#2072)
Browse files Browse the repository at this point in the history
  • Loading branch information
ycheng-kickstarter authored Jul 23, 2024
1 parent b632bb7 commit 30207ed
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 103 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ dependencies {

// Paging 3 for compose dependency
implementation "androidx.paging:paging-compose:3.3.0"
implementation "androidx.paging:paging-testing:3.3.0"

// Testing
testImplementation "junit:junit:4.13.2"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.kickstarter.features.pledgedprojectsoverview.data

data class PledgedProjectsOverviewQueryData(
val first: Int?,
val after: String?,
val last: Int?,
val before: String?
val first: Int? = null,
val after: String? = null,
val last: Int? = null,
val before: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import com.kickstarter.features.pledgedprojectsoverview.data.PledgedProjectsOverviewQueryData
import com.kickstarter.features.pledgedprojectsoverview.viewmodel.PledgedProjectsOverviewViewModel
import com.kickstarter.libs.MessagePreviousScreenType
import com.kickstarter.libs.RefTag
Expand Down Expand Up @@ -48,11 +48,7 @@ class PledgedProjectsOverviewActivity : AppCompatActivity() {
val data = result.data?.getBooleanExtra(IntentKey.FIX_PAYMENT_SUCCESS, false)
data?.let {
if (it.isTrue()) {
viewModel.getPledgedProjects(
PledgedProjectsOverviewQueryData(
25, null, null, null
)
)
viewModel.getPledgedProjects()
}
}
}
Expand All @@ -74,11 +70,13 @@ class PledgedProjectsOverviewActivity : AppCompatActivity() {
val darkModeEnabled = this.isDarkModeEnabled(env = env)
val lazyListState = rememberLazyListState()
val snackbarHostState = remember { SnackbarHostState() }
val totalAlerts = viewModel.totalAlertsState.collectAsStateWithLifecycle().value

val ppoCardPagingSource = viewModel.ppoCardsState.collectAsLazyPagingItems()
val totalAlerts = ppoUIState.totalAlerts
val isLoading = ppoUIState.isLoading || !ppoCardPagingSource.loadState.isIdle

val isLoading = ppoUIState.isLoading || ppoCardPagingSource.loadState.append is LoadState.Loading || ppoCardPagingSource.loadState.refresh is LoadState.Loading
val isErrored = ppoUIState.isErrored || ppoCardPagingSource.loadState.hasError
val showEmptyState = ppoCardPagingSource.loadState.refresh is LoadState.NotLoading && ppoCardPagingSource.itemCount == 0

KickstarterApp(
useDarkTheme =
Expand All @@ -105,6 +103,7 @@ class PledgedProjectsOverviewActivity : AppCompatActivity() {
onSendMessageClick = { projectName -> viewModel.onMessageCreatorClicked(projectName) },
isLoading = isLoading,
isErrored = isErrored,
showEmptyState = showEmptyState,
onSeeAllBackedProjectsClick = { startProfileActivity() },
pullRefreshCallback = {
// TODO call viewmodel.getPledgedProjects() here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import androidx.paging.compose.collectAsLazyPagingItems
import com.kickstarter.R
import com.kickstarter.features.pledgedprojectsoverview.data.PPOCard
import com.kickstarter.features.pledgedprojectsoverview.data.PPOCardFactory
import com.kickstarter.libs.utils.extensions.isNullOrZero
import com.kickstarter.ui.compose.designsystem.KSAlertDialog
import com.kickstarter.ui.compose.designsystem.KSCircularProgressIndicator
import com.kickstarter.ui.compose.designsystem.KSPrimaryGreenButton
Expand Down Expand Up @@ -154,6 +153,7 @@ fun PledgedProjectsOverviewScreen(
onSeeAllBackedProjectsClick: () -> Unit,
isLoading: Boolean = false,
isErrored: Boolean = false,
showEmptyState: Boolean = false,
pullRefreshCallback: () -> Unit = {},
onFixPaymentClick: (projectSlug: String) -> Unit,
) {
Expand Down Expand Up @@ -190,7 +190,7 @@ fun PledgedProjectsOverviewScreen(
) { padding ->
if (isErrored) {
PPOScreenErrorState()
} else if (totalAlerts == 0 || ppoCards.itemCount.isNullOrZero()) {
} else if (showEmptyState) {
PPOScreenEmptyState(onSeeAllBackedProjectsClick)
} else {
LazyColumn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@ package com.kickstarter.features.pledgedprojectsoverview.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.kickstarter.R
import com.kickstarter.features.pledgedprojectsoverview.data.PPOCard
import com.kickstarter.features.pledgedprojectsoverview.data.PledgedProjectsOverviewQueryData
import com.kickstarter.libs.Environment
import com.kickstarter.models.Project
import com.kickstarter.services.ApolloClientTypeV2
import com.kickstarter.services.apiresponses.commentresponse.PageInfoEnvelope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
Expand All @@ -18,6 +25,7 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
Expand All @@ -26,23 +34,67 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx2.asFlow

private val PAGE_LIMIT = 25
class PledgedProjectsPagingSource(
private val apolloClient: ApolloClientTypeV2,
private var totalAlerts: MutableStateFlow<Int>,
private val limit: Int = PAGE_LIMIT,

) : PagingSource<String, PPOCard>() {
override fun getRefreshKey(state: PagingState<String, PPOCard>): String {
return "" // - Default first page is empty string when paginating with graphQL
}

override suspend fun load(params: LoadParams<String>): LoadResult<String, PPOCard> {
return try {
var ppoCardsList = emptyList<PPOCard>()
var nextPageEnvelope: PageInfoEnvelope? = null
var inputData = PledgedProjectsOverviewQueryData(limit, params.key ?: "")
var result: LoadResult<String, PPOCard> = LoadResult.Error(Throwable())

apolloClient.getPledgedProjectsOverviewPledges(
inputData = inputData
)
.asFlow()
.catch {
result = LoadResult.Error(it)
}
.collect { envelope ->
totalAlerts.emit(envelope.totalCount ?: 0)
ppoCardsList = envelope.pledges() ?: emptyList()
nextPageEnvelope = if (envelope.pageInfoEnvelope?.hasNextPage == true) envelope.pageInfoEnvelope else null
result = LoadResult.Page(
data = ppoCardsList,
prevKey = null,
nextKey = nextPageEnvelope?.endCursor
)
}
return result
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}

data class PledgedProjectsOverviewUIState(
var totalAlerts: Int = 0,
val isLoading: Boolean = false,
val isErrored: Boolean = false,
)

class PledgedProjectsOverviewViewModel(environment: Environment) : ViewModel() {

private val mutablePpoCards = MutableStateFlow<PagingData<PPOCard>>(PagingData.empty())
private var mutableProjectFlow = MutableSharedFlow<Project>()
private var snackbarMessage: (stringID: Int) -> Unit = {}
private var totalAlerts = 0
private val apolloClient = requireNotNull(environment.apolloClientV2())

private val mutableTotalAlerts = MutableStateFlow<Int>(0)
val totalAlertsState = mutableTotalAlerts.asStateFlow()

private val mutablePPOUIState = MutableStateFlow(PledgedProjectsOverviewUIState())
val ppoCardsState: StateFlow<PagingData<PPOCard>> = mutablePpoCards.asStateFlow()

private var pagingSource = PledgedProjectsPagingSource(apolloClient, mutableTotalAlerts, PAGE_LIMIT)

val ppoUIState: StateFlow<PledgedProjectsOverviewUIState>
get() = mutablePPOUIState
.asStateFlow()
Expand All @@ -54,7 +106,6 @@ class PledgedProjectsOverviewViewModel(environment: Environment) : ViewModel() {

fun showSnackbarAndRefreshCardsList() {
snackbarMessage.invoke(R.string.address_confirmed_snackbar_text_fpo)

// TODO: MBL-1556 refresh the PPO list (i.e. requery the PPO list).
}

Expand All @@ -66,10 +117,34 @@ class PledgedProjectsOverviewViewModel(environment: Environment) : ViewModel() {
started = SharingStarted.WhileSubscribed(),
)

class Factory(private val environment: Environment) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return PledgedProjectsOverviewViewModel(environment) as T
init {
getPledgedProjects()
}

fun getPledgedProjects() {
viewModelScope.launch(Dispatchers.IO) {
try {
Pager(
PagingConfig(
pageSize = PAGE_LIMIT,
prefetchDistance = 3,
enablePlaceholders = true,
)
) {
pagingSource
}
.flow
.onStart {
emitCurrentState(isLoading = true)
}.catch {
emitCurrentState(isErrored = true)
}.collectLatest { pagingData ->
mutablePpoCards.value = pagingData
emitCurrentState()
}
} catch (e: Exception) {
emitCurrentState(isErrored = true)
}
}
}

Expand All @@ -95,32 +170,19 @@ class PledgedProjectsOverviewViewModel(environment: Environment) : ViewModel() {
}
}

fun getPledgedProjects(inputData: PledgedProjectsOverviewQueryData) {
viewModelScope.launch {
// TODO how we are fetching the data will be modified once the pagination piece in MBL-1473 is finished
apolloClient.getPledgedProjectsOverviewPledges(
inputData = inputData,
)
.asFlow()
.onStart {
emitCurrentState(isLoading = true)
}.map { ppoEnvelope ->
// update paginated ppo card list here
totalAlerts = ppoEnvelope.totalCount() ?: 0
emitCurrentState()
}.catch {
emitCurrentState(isErrored = true)
}.collect()
}
}

private suspend fun emitCurrentState(isLoading: Boolean = false, isErrored: Boolean = false) {
mutablePPOUIState.emit(
PledgedProjectsOverviewUIState(
isLoading = isLoading,
isErrored = isErrored,
totalAlerts = totalAlerts
)
)
}

class Factory(private val environment: Environment) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return PledgedProjectsOverviewViewModel(environment) as T
}
}
}
Loading

0 comments on commit 30207ed

Please sign in to comment.