From 5493ea7c4102815268b451a2ed614e83819defb0 Mon Sep 17 00:00:00 2001 From: stslex Date: Tue, 30 Apr 2024 13:25:59 +0300 Subject: [PATCH] fix paging --- .../ui/components/FavouriteScreen.kt | 2 +- .../content/FavouriteScreenContent.kt | 10 +- .../domain/model/FollowerMapper.kt | 13 -- .../ui/FollowerScreen.kt | 4 +- .../ui/model/FollowerUiMapper.kt | 10 + .../ui/store/FollowerScreenState.kt | 41 ++++ .../ui/store/FollowerStore.kt | 47 ++--- .../ui/store/FollowerStoreImpl.kt | 188 +++++++++--------- .../feature/match/ui/store/MatchStoreImpl.kt | 2 + 9 files changed, 175 insertions(+), 142 deletions(-) delete mode 100644 feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/model/FollowerMapper.kt create mode 100644 feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/model/FollowerUiMapper.kt create mode 100644 feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerScreenState.kt diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/components/FavouriteScreen.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/components/FavouriteScreen.kt index cb540da3..368dc166 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/components/FavouriteScreen.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/components/FavouriteScreen.kt @@ -25,7 +25,7 @@ internal fun FavouriteScreen( when (state.screen) { is FavouriteScreenState.Content -> FavouriteScreenContent( state = state.screen, - items = state.pagingState, + items = state.pagingState.result, query = state.query, isLoading = state.isLoading, onItemClick = { uuid -> diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/components/content/FavouriteScreenContent.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/components/content/FavouriteScreenContent.kt index bdd81e11..661ac7f9 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/components/content/FavouriteScreenContent.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/components/content/FavouriteScreenContent.kt @@ -37,7 +37,7 @@ internal fun FavouriteScreenContent( onSearch = onSearch, ) when (state) { - FavouriteScreenState.Content.Empty -> { + FavouriteScreenState.Empty -> { Column { if (isLoading) { FavouriteScreenShimmer() @@ -88,8 +88,14 @@ internal fun FavouriteScreenContent( } } } - } + FavouriteScreenState.Content.Loading -> { +// TODO() + } + FavouriteScreenState.Content.Refresh -> { +// TODO() + } + } } } } diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/model/FollowerMapper.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/model/FollowerMapper.kt deleted file mode 100644 index 57293647..00000000 --- a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/domain/model/FollowerMapper.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.stslex.feature.follower.domain.model - -import com.stslex.feature.follower.data.model.FollowerDataModel -import com.stslex.feature.follower.ui.model.FollowerModel - -fun List.toUI(): List = map { it.toUI() } - -fun FollowerDataModel.toUI(): FollowerModel = FollowerModel( - uuid = uuid, - username = username, - avatarUrl = avatarUrl, - isFollowing = isFollowing -) \ No newline at end of file diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreen.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreen.kt index 276ea8df..1e0a8772 100644 --- a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreen.kt +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreen.kt @@ -48,9 +48,9 @@ internal fun FollowerScreen( when (state.screen) { is FollowerScreenState.Content -> { LazyColumn { - items(state.data.size) { index -> + items(state.pagingState.result.size) { index -> Text( - "test: ${state.data[index].username}" + "test: ${state.pagingState.result[index].username}" ) } } diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/model/FollowerUiMapper.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/model/FollowerUiMapper.kt new file mode 100644 index 00000000..fddc1322 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/model/FollowerUiMapper.kt @@ -0,0 +1,10 @@ +package com.stslex.feature.follower.ui.model + +import com.stslex.feature.follower.data.model.FollowerDataModel + +fun FollowerDataModel.toUi(): FollowerModel = FollowerModel( + uuid = uuid, + username = username, + avatarUrl = avatarUrl, + isFollowing = isFollowing +) \ No newline at end of file diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerScreenState.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerScreenState.kt new file mode 100644 index 00000000..55293f28 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerScreenState.kt @@ -0,0 +1,41 @@ +package com.stslex.feature.follower.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.base.AppError +import com.stslex.core.ui.pager.states.PagerLoadState + +@Stable +sealed interface FollowerScreenState { + + @Stable + sealed interface Content : FollowerScreenState { + + @Stable + data object Data : Content + + @Stable + data object Loading : Content + + @Stable + data object Refresh : Content + } + + @Stable + data object Shimmer : FollowerScreenState + + @Stable + data object Empty : FollowerScreenState + + @Stable + data class Error(val error: AppError) : FollowerScreenState +} + +fun PagerLoadState.toUi(): FollowerScreenState = when (this) { + is PagerLoadState.Loading -> FollowerScreenState.Content.Loading + is PagerLoadState.Error -> FollowerScreenState.Error(error) + is PagerLoadState.Initial -> FollowerScreenState.Shimmer + is PagerLoadState.Empty -> FollowerScreenState.Empty + PagerLoadState.Data -> FollowerScreenState.Content.Data + PagerLoadState.Refresh -> FollowerScreenState.Content.Refresh + PagerLoadState.Retry -> FollowerScreenState.Shimmer +} \ No newline at end of file diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStore.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStore.kt index 2a31ca70..86e9db10 100644 --- a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStore.kt +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStore.kt @@ -1,23 +1,21 @@ package com.stslex.feature.follower.ui.store import androidx.compose.runtime.Stable +import com.stslex.core.ui.base.paging.PagingState import com.stslex.core.ui.mvi.Store +import com.stslex.core.ui.mvi.Store.Event.Snackbar import com.stslex.feature.follower.navigation.FollowerScreenArgs import com.stslex.feature.follower.ui.model.FollowerModel import com.stslex.feature.follower.ui.store.FollowerStore.Action import com.stslex.feature.follower.ui.store.FollowerStore.Event import com.stslex.feature.follower.ui.store.FollowerStore.State -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList interface FollowerStore : Store { @Stable data class State( - val uuid: String, - val page: Int, val type: FollowerScreenArgs, - val data: ImmutableList, + val pagingState: PagingState, val screen: FollowerScreenState, val query: String ) : Store.State { @@ -27,10 +25,8 @@ interface FollowerStore : Store { const val DEFAULT_PAGE = -1 val INITIAL = State( - uuid = "", - page = DEFAULT_PAGE, type = FollowerScreenArgs.Follower(""), - data = emptyList().toImmutableList(), + pagingState = PagingState.default(), screen = FollowerScreenState.Shimmer, query = "" ) @@ -46,38 +42,27 @@ interface FollowerStore : Store { ) : Action @Stable - data object LoadMore : Action - } - - @Stable - sealed interface Event : Store.Event { + data object Load : Action @Stable - data class ErrorSnackBar(val message: String) : Event - } - - sealed interface Navigation : Store.Navigation -} - -@Stable -sealed interface FollowerScreenState { + data object Refresh : Action - @Stable - sealed interface Content : FollowerScreenState { + @Stable + data object Retry : Action @Stable - data object NotLoading : Content + data class QueryChanged(val query: String) : Action @Stable - data object Loading : Content + data class OnUserClick(val uuid: String) : Action } @Stable - data object Shimmer : FollowerScreenState + sealed interface Event : Store.Event { - @Stable - data object Empty : FollowerScreenState + @Stable + data class ShowSnackbar(val snackbar: Snackbar) : Event + } - @Stable - data class Error(val error: Throwable) : FollowerScreenState -} \ No newline at end of file + sealed interface Navigation : Store.Navigation +} diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreImpl.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreImpl.kt index 18c09deb..5b1be04e 100644 --- a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreImpl.kt +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreImpl.kt @@ -1,17 +1,21 @@ package com.stslex.feature.follower.ui.store import com.stslex.core.core.AppDispatcher +import com.stslex.core.ui.base.mapToAppError import com.stslex.core.ui.mvi.BaseStore +import com.stslex.core.ui.mvi.Store.Event.Snackbar +import com.stslex.core.ui.pager.pager.StorePager +import com.stslex.core.ui.pager.pager.StorePagerFactory +import com.stslex.core.ui.pager.states.PagerLoadState import com.stslex.feature.follower.domain.interactor.FollowerInteractor import com.stslex.feature.follower.navigation.FollowerRouter import com.stslex.feature.follower.navigation.FollowerScreenArgs +import com.stslex.feature.follower.ui.model.FollowerModel +import com.stslex.feature.follower.ui.model.toUi import com.stslex.feature.follower.ui.store.FollowerStore.Action import com.stslex.feature.follower.ui.store.FollowerStore.Event import com.stslex.feature.follower.ui.store.FollowerStore.Navigation import com.stslex.feature.follower.ui.store.FollowerStore.State -import com.stslex.feature.follower.ui.store.FollowerStore.State.Companion.DEFAULT_PAGE -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -19,126 +23,124 @@ class FollowerStoreImpl( private val interactor: FollowerInteractor, router: FollowerRouter, appDispatcher: AppDispatcher, + pagerFactory: StorePagerFactory, ) : FollowerStore, BaseStore( router = router, appDispatcher = appDispatcher, initialState = State.INITIAL, ) { - private var loadingJob: Job? = null + private val pager: StorePager = pagerFactory.create( + request = { page, pageSize -> + val currentState = state.value + when (currentState.type) { + is FollowerScreenArgs.Follower -> interactor.getFollowers( + uuid = currentState.type.uuid, + query = currentState.query, + page = page, + pageSize = pageSize + ) + + is FollowerScreenArgs.Following -> interactor.getFollowing( + uuid = currentState.type.uuid, + query = currentState.query, + page = page, + pageSize = pageSize + ) + } + }, + scope = scope, + mapper = { it.toUi() } + ) override fun process(action: Action) { when (action) { is Action.Init -> actionInit(action) - is Action.LoadMore -> actionLoadMore() + is Action.Load -> actionLoad() + is Action.OnUserClick -> actionUserClick(action) + is Action.QueryChanged -> actionQueryChanged(action) + Action.Refresh -> actionRefresh() + Action.Retry -> actionRetry() } } - private fun actionLoadMore() { - loadNextItems() - } - private fun actionInit(action: Action.Init) { updateState { state -> - state.copy( - uuid = action.args.uuid, - type = action.args, - screen = FollowerScreenState.Shimmer - ) + state.copy(type = action.args) } - state.map { it.query } - .distinctUntilChanged() - .launch { query -> - updateState { state -> - state.copy( - page = DEFAULT_PAGE, - query = query, - ) - } - loadNextItems() + pager.state.launch { pagerState -> + updateState { currentState -> + currentState.copy( + pagingState = pagerState + ) } + } - interactor.followItems - .launch { data -> - val screen = if (data.isEmpty()) { - FollowerScreenState.Empty + pager.loadState.launch { loadState -> + updateState { currentState -> + currentState.copy( + screen = loadState.toUi() + ) + } + } + + pager.loadEvents.launch { + sendEvent( + Event.ShowSnackbar(Snackbar.Error("error load matches")) + ) + } + + state + .map { it.query } + .distinctUntilChanged() + .launch( + onError = ::showError + ) { + if (pager.loadState.value is PagerLoadState.Initial) { + pager.initialLoad() } else { - FollowerScreenState.Content.NotLoading - } - updateState { state -> - state.copy( - data = data.toImmutableList(), - screen = screen, - ) + pager.refresh(isForceLoad = false) } } } - private fun loadNextItems() { - if (loadingJob?.isActive == true) return + private fun actionLoad() { + pager.load() + } - val currentState = state.value + private fun actionUserClick(action: Action.OnUserClick) { + // todo ("navigate to user") + } - val loadingScreen = if ( - currentState.screen is FollowerScreenState.Content && - currentState.data.isNotEmpty() - ) { - FollowerScreenState.Content.Loading - } else { - FollowerScreenState.Shimmer + private fun actionQueryChanged(action: Action.QueryChanged) { + updateState { currentState -> + currentState.copy( + query = action.query + ) } + } - updateState { state -> state.copy(screen = loadingScreen) } - - val page = if (currentState.page == DEFAULT_PAGE) { - FIRST_PAGE - } else { - currentState.page.inc() - } + private fun actionRefresh() { + pager.refresh(isForceLoad = true) + } - launch( - action = { - when (val type = currentState.type) { - is FollowerScreenArgs.Follower -> interactor.getFollowers( - uuid = type.uuid, - query = "", // todo add query - page = page, - pageSize = PAGE_SIZE - ) - - is FollowerScreenArgs.Following -> interactor.getFollowing( - uuid = type.uuid, - query = "", // todo add query - page = page, - pageSize = PAGE_SIZE - ) - } - }, - onSuccess = { - updateState { state -> - state.copy( - page = page - ) - } - }, - onError = { error -> - if (currentState.data.isEmpty()) { - updateState { state -> - state.copy( - screen = FollowerScreenState.Error(error) - ) - } - } else { - updateState { it.copy(screen = FollowerScreenState.Content.NotLoading) } - sendEvent(Event.ErrorSnackBar(error.message.orEmpty())) - } - } - ) + private fun actionRetry() { + pager.retry() } - companion object { - private const val PAGE_SIZE = 10 - private const val FIRST_PAGE = 1 + private fun showError(error: Throwable) { + val appError = error.mapToAppError("error logout") + if (state.value.screen is FollowerScreenState.Content) { + sendEvent( + Event.ShowSnackbar(Snackbar.Error(appError.message)) + ) + } else { + updateState { currentState -> + currentState.copy( + screen = FollowerScreenState.Error(appError) + ) + } + } } } diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreImpl.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreImpl.kt index 6695a3bc..bfbc71ce 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreImpl.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreImpl.kt @@ -69,6 +69,7 @@ class MatchStoreImpl( ) } } + pager.loadState.launch { loadState -> updateState { currentState -> currentState.copy( @@ -76,6 +77,7 @@ class MatchStoreImpl( ) } } + pager.loadEvents.launch { sendEvent( Event.ShowSnackbar(Snackbar.Error("error load matches"))