From 51851f901be9af909943d2ac4d7eb3a3d7e90b1f Mon Sep 17 00:00:00 2001 From: KwakEuiJin Date: Thu, 15 Feb 2024 16:22:09 +0900 Subject: [PATCH] =?UTF-8?q?[feat/login]:=20=EC=8B=9D=EB=8B=B9=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B8=B0=EB=8A=A5=20Paging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../everymeal_android/di/RepositoryModule.kt | 8 +++ .../datasource/search/SearchDataSource.kt | 15 ++++ .../datasource/search/SearchDataSourceImpl.kt | 35 ++++++++++ .../datasource/search/SearchPagingSource.kt | 46 +++++++++++++ .../restaruant/SearchRestaurantResponse.kt | 68 +++++-------------- .../search/DefaultSearchRepository.kt | 24 +++++-- .../data/service/search/SearchService.kt | 7 +- .../repository/search/SearchRepository.kt | 4 +- .../presentation/ui/home/HomeViewModel.kt | 1 - .../presentation/ui/search/SearchContract.kt | 5 +- .../presentation/ui/search/SearchScreen.kt | 18 +++-- .../presentation/ui/search/SearchViewModel.kt | 14 +++- 12 files changed, 174 insertions(+), 71 deletions(-) create mode 100644 data/src/main/java/com/everymeal/data/datasource/search/SearchDataSource.kt create mode 100644 data/src/main/java/com/everymeal/data/datasource/search/SearchDataSourceImpl.kt create mode 100644 data/src/main/java/com/everymeal/data/datasource/search/SearchPagingSource.kt diff --git a/app/src/main/java/com/everymeal/everymeal_android/di/RepositoryModule.kt b/app/src/main/java/com/everymeal/everymeal_android/di/RepositoryModule.kt index fc38be9c..93b3546b 100644 --- a/app/src/main/java/com/everymeal/everymeal_android/di/RepositoryModule.kt +++ b/app/src/main/java/com/everymeal/everymeal_android/di/RepositoryModule.kt @@ -10,6 +10,8 @@ import com.everymeal.data.datasource.restaurant.RestaurantDataSource import com.everymeal.data.datasource.restaurant.RestaurantDataSourceImpl import com.everymeal.data.datasource.review.ReviewDataSource import com.everymeal.data.datasource.review.ReviewDataSourceImpl +import com.everymeal.data.datasource.search.SearchDataSource +import com.everymeal.data.datasource.search.SearchDataSourceImpl import com.everymeal.data.repository.auth.DefaultUsersRepository import com.everymeal.data.repository.local.LocalRepositoryImpl import com.everymeal.data.repository.onboarding.OnboardingRepositoryImpl @@ -92,6 +94,12 @@ abstract class RepositoryModule { defaultReviewRepository: DefaultReviewRepository, ): ReviewRepository + @Singleton + @Binds + abstract fun bindSearchDataSource( + searchDataSourceImpl: SearchDataSourceImpl, + ): SearchDataSource + @Singleton @Binds abstract fun bindSearchRepository( diff --git a/data/src/main/java/com/everymeal/data/datasource/search/SearchDataSource.kt b/data/src/main/java/com/everymeal/data/datasource/search/SearchDataSource.kt new file mode 100644 index 00000000..a828c979 --- /dev/null +++ b/data/src/main/java/com/everymeal/data/datasource/search/SearchDataSource.kt @@ -0,0 +1,15 @@ +package com.everymeal.data.datasource.search + +import androidx.paging.PagingData +import com.everymeal.data.model.restaruant.RestaurantResponse +import kotlinx.coroutines.flow.Flow + +interface SearchDataSource { + + suspend fun searchRestraurant( + campusIdx: Int, + keyword: String, + ): Flow> + + +} diff --git a/data/src/main/java/com/everymeal/data/datasource/search/SearchDataSourceImpl.kt b/data/src/main/java/com/everymeal/data/datasource/search/SearchDataSourceImpl.kt new file mode 100644 index 00000000..6746feba --- /dev/null +++ b/data/src/main/java/com/everymeal/data/datasource/search/SearchDataSourceImpl.kt @@ -0,0 +1,35 @@ +package com.everymeal.data.datasource.search + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.everymeal.data.model.restaruant.RestaurantResponse +import com.everymeal.data.service.search.SearchService +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class SearchDataSourceImpl @Inject constructor( + private val searchService: SearchService, +) : SearchDataSource { + override suspend fun searchRestraurant( + campusIdx: Int, + keyword: String, + ): Flow> { + val pagingSourceFactory = { + SearchPagingSource( + searchService = searchService, + campusIdx = campusIdx, + keyword = keyword + ) + } + return Pager( + config = PagingConfig( + initialLoadSize = 20, + pageSize = 20, + enablePlaceholders = false, + ), + pagingSourceFactory = pagingSourceFactory + ).flow + } + +} diff --git a/data/src/main/java/com/everymeal/data/datasource/search/SearchPagingSource.kt b/data/src/main/java/com/everymeal/data/datasource/search/SearchPagingSource.kt new file mode 100644 index 00000000..da86a7af --- /dev/null +++ b/data/src/main/java/com/everymeal/data/datasource/search/SearchPagingSource.kt @@ -0,0 +1,46 @@ +package com.everymeal.data.datasource.search + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.everymeal.data.datasource.restaurant.PAGING_SIZE +import com.everymeal.data.datasource.restaurant.STARTING_PAGE_INDEX +import com.everymeal.data.model.restaruant.RestaurantResponse +import com.everymeal.data.service.search.SearchService +import retrofit2.HttpException +import java.io.IOException + +class SearchPagingSource( + private val searchService: SearchService, + private val campusIdx: Int, + private val keyword: String, +) : 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 { + val position = params.key ?: STARTING_PAGE_INDEX + + return try { + val response = searchService.search( + campusIdx = campusIdx, + keyword = keyword, + offset = position, + limit = PAGING_SIZE + ) + val restaurants = response.data?.content ?: emptyList() + LoadResult.Page( + data = restaurants, + prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1, + nextKey = if (restaurants.isEmpty()) null else position + 1 + ) + } catch (exception: IOException) { + LoadResult.Error(exception) + } catch (exception: HttpException) { + LoadResult.Error(exception) + } + } +} diff --git a/data/src/main/java/com/everymeal/data/model/restaruant/SearchRestaurantResponse.kt b/data/src/main/java/com/everymeal/data/model/restaruant/SearchRestaurantResponse.kt index 0c8fb431..ea929c0f 100644 --- a/data/src/main/java/com/everymeal/data/model/restaruant/SearchRestaurantResponse.kt +++ b/data/src/main/java/com/everymeal/data/model/restaruant/SearchRestaurantResponse.kt @@ -16,7 +16,7 @@ data class SearchRestaurantResponse( @Serializable data class Data( @SerialName("content") - val content: List? = null, + val content: List? = null, @SerialName("empty") val empty: Boolean? = null, @SerialName("first") @@ -32,37 +32,12 @@ data class SearchRestaurantResponse( @SerialName("size") val size: Int? = null, @SerialName("sort") - val sort: Sort? = null, + val sort: Pageable.Sort? = null, @SerialName("totalElements") val totalElements: Int? = null, @SerialName("totalPages") val totalPages: Int? = null, ) { - @Serializable - data class Content( - @SerialName("address") - val address: String? = null, - @SerialName("categoryDetail") - val categoryDetail: String? = null, - @SerialName("distance") - val distance: Int? = null, - @SerialName("grade") - val grade: Int? = null, - @SerialName("idx") - val idx: Int? = null, - @SerialName("images") - val images: List? = null, - @SerialName("isLiked") - val isLiked: Boolean? = null, - @SerialName("name") - val name: String? = null, - @SerialName("phoneNumber") - val phoneNumber: String? = null, - @SerialName("recommendedCount") - val recommendedCount: Int? = null, - @SerialName("reviewCount") - val reviewCount: Int? = null, - ) @Serializable data class Pageable( @@ -89,36 +64,25 @@ data class SearchRestaurantResponse( val unsorted: Boolean? = null, ) } - - @Serializable - data class Sort( - @SerialName("empty") - val empty: Boolean? = null, - @SerialName("sorted") - val sorted: Boolean? = null, - @SerialName("unsorted") - val unsorted: Boolean? = null, - ) } } -fun SearchRestaurantResponse.toRestaurants(): List { - return this.data?.content?.mapNotNull { content -> - content?.let { +fun SearchRestaurantResponse.toRestaurants(): List { + return this.data?.content?.map { content -> + content.let { RestaurantDataEntity( - idx = it.idx ?: 0, - name = it.name.orEmpty(), - address = it.address.orEmpty(), - phoneNumber = it.phoneNumber.orEmpty(), - categoryDetail = it.categoryDetail.orEmpty(), - distance = it.distance ?: 0, - grade = it.grade?.toFloat() - ?: 0f, - reviewCount = it.reviewCount ?: 0, - recommendedCount = it.recommendedCount ?: 0, - images = it.images?.filterNotNull(), - isLiked = it.isLiked ?: false, + idx = it.idx, + name = it.name, + address = it.address, + phoneNumber = it.phoneNumber, + categoryDetail = it.categoryDetail, + distance = it.distance, + grade = it.grade, + reviewCount = it.reviewCount, + recommendedCount = it.recommendedCount, + images = it.images, + isLiked = it.isLiked, ) } } ?: emptyList() diff --git a/data/src/main/java/com/everymeal/data/repository/search/DefaultSearchRepository.kt b/data/src/main/java/com/everymeal/data/repository/search/DefaultSearchRepository.kt index 926818ed..dbfcd6c6 100644 --- a/data/src/main/java/com/everymeal/data/repository/search/DefaultSearchRepository.kt +++ b/data/src/main/java/com/everymeal/data/repository/search/DefaultSearchRepository.kt @@ -1,17 +1,29 @@ package com.everymeal.data.repository.search -import com.everymeal.data.model.restaruant.toRestaurants -import com.everymeal.data.service.search.SearchService +import androidx.paging.PagingData +import androidx.paging.map +import com.everymeal.data.datasource.search.SearchDataSourceImpl +import com.everymeal.data.model.restaruant.toRestaurant import com.everymeal.domain.model.restaurant.RestaurantDataEntity import com.everymeal.domain.repository.search.SearchRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import javax.inject.Inject class DefaultSearchRepository @Inject constructor( - private val searchService: SearchService, + private val searchDataSourceImpl: SearchDataSourceImpl, ) : SearchRepository { - override suspend fun search(keyword: String): Result> { - return runCatching { - searchService.search(keyword).toRestaurants() + override suspend fun search( + campusIdx: Int, + keyword: String + ): Flow> { + return searchDataSourceImpl.searchRestraurant( + campusIdx = campusIdx, + keyword = keyword, + ).map { pagingData -> + pagingData.map { + it.toRestaurant() + } } } } diff --git a/data/src/main/java/com/everymeal/data/service/search/SearchService.kt b/data/src/main/java/com/everymeal/data/service/search/SearchService.kt index ef02afa8..4b29f2c8 100644 --- a/data/src/main/java/com/everymeal/data/service/search/SearchService.kt +++ b/data/src/main/java/com/everymeal/data/service/search/SearchService.kt @@ -1,13 +1,18 @@ package com.everymeal.data.service.search +import com.everymeal.data.model.BaseResponse import com.everymeal.data.model.restaruant.SearchRestaurantResponse import retrofit2.http.GET import retrofit2.http.Path +import retrofit2.http.Query interface SearchService { // TODO 임시 캠퍼스 ID - @GET("/api/v1/stores/{0}/{keyword}") + @GET("/api/v1/stores/{campusIdx}/{keyword}") suspend fun search( + @Path("campusIdx") campusIdx: Int, @Path("keyword") keyword: String, + @Query("offset") offset: Int?, + @Query("limit") limit: Int?, ): SearchRestaurantResponse } diff --git a/domain/src/main/java/com/everymeal/domain/repository/search/SearchRepository.kt b/domain/src/main/java/com/everymeal/domain/repository/search/SearchRepository.kt index 50f79e49..6350436b 100644 --- a/domain/src/main/java/com/everymeal/domain/repository/search/SearchRepository.kt +++ b/domain/src/main/java/com/everymeal/domain/repository/search/SearchRepository.kt @@ -1,7 +1,9 @@ package com.everymeal.domain.repository.search +import androidx.paging.PagingData import com.everymeal.domain.model.restaurant.RestaurantDataEntity +import kotlinx.coroutines.flow.Flow interface SearchRepository { - suspend fun search(keyword: String): Result> + suspend fun search(campusIdx: Int, keyword: String): Flow> } diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/home/HomeViewModel.kt b/presentation/src/main/java/com/everymeal/presentation/ui/home/HomeViewModel.kt index a0cafadf..6384f99b 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/home/HomeViewModel.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/home/HomeViewModel.kt @@ -4,7 +4,6 @@ import android.util.Log import androidx.lifecycle.viewModelScope import com.everymeal.domain.usecase.local.GetUniversityIndexUseCase import com.everymeal.domain.usecase.restaurant.GetHomeRestaurantUseCase -import com.everymeal.domain.usecase.restaurant.GetUnivRestaurantUseCase import com.everymeal.domain.usecase.review.GetHomeReviewUseCase import com.everymeal.presentation.base.BaseViewModel import com.everymeal.presentation.base.LoadState diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/search/SearchContract.kt b/presentation/src/main/java/com/everymeal/presentation/ui/search/SearchContract.kt index f217cacc..7d1cc089 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/search/SearchContract.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/search/SearchContract.kt @@ -1,9 +1,12 @@ package com.everymeal.presentation.ui.search +import androidx.paging.PagingData import com.everymeal.domain.model.restaurant.RestaurantDataEntity import com.everymeal.presentation.base.ViewEvent import com.everymeal.presentation.base.ViewSideEffect import com.everymeal.presentation.base.ViewState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow /* 대학교 불러오기 LoadState @@ -13,7 +16,7 @@ data class SearchState( val searchQuery: String = "", val searchIsShowHistory: Boolean = true, val searchHistoryItems: List = listOf(), - val searchResultList: List = listOf(), + val searchResultList: Flow> = flow { }, ) : ViewState sealed class SearchEvent : ViewEvent { diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/search/SearchScreen.kt b/presentation/src/main/java/com/everymeal/presentation/ui/search/SearchScreen.kt index 1474f54e..13ba2e23 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/search/SearchScreen.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/search/SearchScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -29,6 +28,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems import com.everymeal.domain.model.restaurant.RestaurantDataEntity import com.everymeal.presentation.R import com.everymeal.presentation.components.EveryMealRestaurantItem @@ -75,7 +76,7 @@ fun SearchScreen( } else { SearchDetail( modifier = Modifier.padding(innerPadding), - searchResultList = viewState.value.searchResultList, + searchResultList = viewState.value.searchResultList.collectAsLazyPagingItems(), ) } } @@ -84,14 +85,19 @@ fun SearchScreen( @Composable fun SearchDetail( modifier: Modifier = Modifier, - searchResultList: List, + searchResultList: LazyPagingItems, ) { LazyColumn(modifier = modifier) { - itemsIndexed(searchResultList) { index, restaurant -> + items(searchResultList.itemCount) { indexd -> + val restaurant = searchResultList[indexd] ?: return@items EveryMealRestaurantItem( restaurant = restaurant, - onLoveClick = { }, - onDetailClick = { }, + onLoveClick = { + + }, + onDetailClick = { + + }, ) } } diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/search/SearchViewModel.kt b/presentation/src/main/java/com/everymeal/presentation/ui/search/SearchViewModel.kt index 415ccd4a..907f6fe7 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/search/SearchViewModel.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/search/SearchViewModel.kt @@ -1,15 +1,21 @@ package com.everymeal.presentation.ui.search import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn import com.everymeal.domain.repository.search.SearchRepository +import com.everymeal.domain.usecase.local.GetUniversityIndexUseCase +import com.everymeal.domain.usecase.onboarding.GetUniversityUseCase import com.everymeal.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( private val searchRepository: SearchRepository, + private val getUniversityIndexUseCase: GetUniversityIndexUseCase, ) : BaseViewModel(SearchState()) { init { @@ -44,17 +50,19 @@ class SearchViewModel @Inject constructor( private fun search() { viewModelScope.launch { val keyword = viewState.value.searchQuery + val campusIdx = getUniversityIndexUseCase().first().toInt() if (keyword.isEmpty()) { return@launch } - searchRepository.search(keyword).onSuccess { result -> + + val searchResultList = searchRepository.search(campusIdx, keyword).cachedIn(viewModelScope) updateState { copy( - searchResultList = result, + searchResultList = searchResultList, searchIsShowHistory = false, ) } - } + } } }