diff --git a/build-logic/src/main/java/terning.kotlin.library.gradle.kts b/build-logic/src/main/java/terning.kotlin.library.gradle.kts index adb1b0651..a02ef1c4e 100644 --- a/build-logic/src/main/java/terning.kotlin.library.gradle.kts +++ b/build-logic/src/main/java/terning.kotlin.library.gradle.kts @@ -1,3 +1,4 @@ +import com.terning.build_logic.convention.configureCoroutineKotlin import com.terning.build_logic.convention.configureKotlin plugins { diff --git a/data/home/build.gradle.kts b/data/home/build.gradle.kts index 13407d4bb..67e468813 100644 --- a/data/home/build.gradle.kts +++ b/data/home/build.gradle.kts @@ -11,4 +11,7 @@ android { dependencies { // domain implementation(projects.domain.home) + + //paging + implementation(libs.paging.runtime) } \ No newline at end of file diff --git a/data/home/src/main/java/com/terning/data/home/datasource/HomeDataSource.kt b/data/home/src/main/java/com/terning/data/home/datasource/HomeDataSource.kt index 0c255517c..fbc6e6125 100644 --- a/data/home/src/main/java/com/terning/data/home/datasource/HomeDataSource.kt +++ b/data/home/src/main/java/com/terning/data/home/datasource/HomeDataSource.kt @@ -12,6 +12,7 @@ interface HomeDataSource { suspend fun getRecommendIntern( sortBy: String, + page: Int, ): BaseResponse suspend fun getFilteringInfo(): BaseResponse diff --git a/data/home/src/main/java/com/terning/data/home/datasourceimpl/HomeDataSourceImpl.kt b/data/home/src/main/java/com/terning/data/home/datasourceimpl/HomeDataSourceImpl.kt index 973151a64..e90b1803c 100644 --- a/data/home/src/main/java/com/terning/data/home/datasourceimpl/HomeDataSourceImpl.kt +++ b/data/home/src/main/java/com/terning/data/home/datasourceimpl/HomeDataSourceImpl.kt @@ -18,9 +18,11 @@ class HomeDataSourceImpl @Inject constructor( override suspend fun getRecommendIntern( sortBy: String, + page: Int, ): BaseResponse = homeService.getRecommendIntern( sortBy = sortBy, + page = page ) override suspend fun getFilteringInfo(): BaseResponse = diff --git a/data/home/src/main/java/com/terning/data/home/dto/response/HomeRecommendInternResponseDto.kt b/data/home/src/main/java/com/terning/data/home/dto/response/HomeRecommendInternResponseDto.kt index 1cf736f04..8039cc406 100644 --- a/data/home/src/main/java/com/terning/data/home/dto/response/HomeRecommendInternResponseDto.kt +++ b/data/home/src/main/java/com/terning/data/home/dto/response/HomeRecommendInternResponseDto.kt @@ -7,6 +7,8 @@ import kotlinx.serialization.Serializable data class HomeRecommendInternResponseDto( @SerialName("totalCount") val totalCount: Int, + @SerialName("hasNext") + val hasNextPage: Boolean, @SerialName("result") val result: List ) { diff --git a/data/home/src/main/java/com/terning/data/home/mapper/HomeRecommendInternMapper.kt b/data/home/src/main/java/com/terning/data/home/mapper/HomeRecommendInternMapper.kt index 00f4d2b93..ba6e16732 100644 --- a/data/home/src/main/java/com/terning/data/home/mapper/HomeRecommendInternMapper.kt +++ b/data/home/src/main/java/com/terning/data/home/mapper/HomeRecommendInternMapper.kt @@ -1,25 +1,19 @@ package com.terning.data.home.mapper import com.terning.data.home.dto.response.HomeRecommendInternResponseDto -import com.terning.domain.home.entity.HomeRecommendIntern +import com.terning.domain.home.entity.HomeRecommendedIntern -fun HomeRecommendInternResponseDto.toHomeRecommendInternList(): HomeRecommendIntern = - HomeRecommendIntern( - totalCount = this.totalCount, - homeRecommendInternDetail = this.result.map { - it.toHomeRecommendInternDetail() - } - ) -fun HomeRecommendInternResponseDto.Result.toHomeRecommendInternDetail(): HomeRecommendIntern.HomeRecommendInternDetail = - HomeRecommendIntern.HomeRecommendInternDetail( +fun HomeRecommendInternResponseDto.Result.toHomeRecommendedIntern(totalCount: Int): HomeRecommendedIntern = + HomeRecommendedIntern( + totalCount = totalCount, internshipAnnouncementId = this.internshipAnnouncementId, - title = this.title, + companyImage = this.companyImage, dDay = this.dDay, - deadline = deadline, + title = this.title, workingPeriod = this.workingPeriod, - startYearMonth = this.startYearMonth, - companyImage = this.companyImage, isScrapped = this.isScrapped, color = this.color, + deadline = this.deadline, + startYearMonth = this.startYearMonth, ) \ No newline at end of file diff --git a/data/home/src/main/java/com/terning/data/home/pagingsource/HomePagingSource.kt b/data/home/src/main/java/com/terning/data/home/pagingsource/HomePagingSource.kt new file mode 100644 index 000000000..421236626 --- /dev/null +++ b/data/home/src/main/java/com/terning/data/home/pagingsource/HomePagingSource.kt @@ -0,0 +1,40 @@ +package com.terning.data.home.pagingsource + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.terning.data.home.datasource.HomeDataSource +import com.terning.data.home.dto.response.HomeRecommendInternResponseDto + +class HomePagingSource( + private val sortBy: String, + private val dataSource: HomeDataSource +) : PagingSource>() { + + + override suspend fun load(params: LoadParams): LoadResult> { + return try { + val nextParamKey = params.key ?: 0 + + val response = dataSource.getRecommendIntern(sortBy = sortBy, page = nextParamKey) + val totalCount = response.result.totalCount + val hasNextPage = response.result.hasNextPage + + LoadResult.Page( + data = response.result.result.map { + Pair(totalCount, it) + }, + prevKey = null, // 다음 페이지 로딩만 가능하도록 설정 + nextKey = if (hasNextPage) nextParamKey + 1 else null + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState>): Int? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) + } + } +} \ No newline at end of file diff --git a/data/home/src/main/java/com/terning/data/home/repositoryimpl/HomeRepositoryImpl.kt b/data/home/src/main/java/com/terning/data/home/repositoryimpl/HomeRepositoryImpl.kt index d06771591..d70fabb7d 100644 --- a/data/home/src/main/java/com/terning/data/home/repositoryimpl/HomeRepositoryImpl.kt +++ b/data/home/src/main/java/com/terning/data/home/repositoryimpl/HomeRepositoryImpl.kt @@ -1,33 +1,45 @@ package com.terning.data.home.repositoryimpl +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.map import com.terning.data.home.datasource.HomeDataSource import com.terning.data.home.dto.request.toChangeFilterRequestDto import com.terning.data.home.mapper.toHomeFilteringInfo -import com.terning.data.home.mapper.toHomeRecommendInternList +import com.terning.data.home.mapper.toHomeRecommendedIntern import com.terning.data.home.mapper.toHomeUpcomingInternList +import com.terning.data.home.pagingsource.HomePagingSource import com.terning.domain.home.entity.ChangeFilteringRequestModel import com.terning.domain.home.entity.HomeFilteringInfo -import com.terning.domain.home.entity.HomeRecommendIntern +import com.terning.domain.home.entity.HomeRecommendedIntern import com.terning.domain.home.entity.HomeUpcomingIntern import com.terning.domain.home.repository.HomeRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import javax.inject.Inject class HomeRepositoryImpl @Inject constructor( private val homeDataSource: HomeDataSource, ) : HomeRepository { + override suspend fun getHomeUpcomingInternList(): Result = runCatching { homeDataSource.getUpcomingIntern().result.toHomeUpcomingInternList() } - override suspend fun getRecommendIntern( - sortBy: String, - ): Result = - runCatching { - homeDataSource.getRecommendIntern( + override fun getRecommendIntern(sortBy: String): Flow> { + return Pager( + PagingConfig(pageSize = 10) + ) { + HomePagingSource( sortBy = sortBy, - ).result.toHomeRecommendInternList() + dataSource = homeDataSource + ) + }.flow.map { pagedData -> + pagedData.map { it.second.toHomeRecommendedIntern(it.first) } } + } override suspend fun getFilteringInfo(): Result = runCatching { diff --git a/data/home/src/main/java/com/terning/data/home/service/HomeService.kt b/data/home/src/main/java/com/terning/data/home/service/HomeService.kt index cc54844be..cb395d3f5 100644 --- a/data/home/src/main/java/com/terning/data/home/service/HomeService.kt +++ b/data/home/src/main/java/com/terning/data/home/service/HomeService.kt @@ -18,6 +18,7 @@ interface HomeService { @GET("api/v1/home") suspend fun getRecommendIntern( @Query("sortBy") sortBy: String, + @Query("page") page: Int, ): BaseResponse @GET("api/v1/filters") diff --git a/domain/home/build.gradle.kts b/domain/home/build.gradle.kts index 0fde5b106..8c7175e69 100644 --- a/domain/home/build.gradle.kts +++ b/domain/home/build.gradle.kts @@ -1,3 +1,9 @@ plugins { alias(libs.plugins.terning.kotlin) +} + +dependencies { + implementation(libs.paging.common) + implementation(libs.coroutines.core) + implementation(libs.coroutines.test) } \ No newline at end of file diff --git a/domain/home/src/main/java/com/terning/domain/home/entity/HomeRecommendedIntern.kt b/domain/home/src/main/java/com/terning/domain/home/entity/HomeRecommendedIntern.kt new file mode 100644 index 000000000..57d077beb --- /dev/null +++ b/domain/home/src/main/java/com/terning/domain/home/entity/HomeRecommendedIntern.kt @@ -0,0 +1,14 @@ +package com.terning.domain.home.entity + +data class HomeRecommendedIntern( + val totalCount: Int, + val internshipAnnouncementId: Long, + val companyImage: String, + val dDay: String, + val title: String, + val workingPeriod: String, + val isScrapped: Boolean, + val color: String?, + val deadline: String, + val startYearMonth: String, +) \ No newline at end of file diff --git a/domain/home/src/main/java/com/terning/domain/home/repository/HomeRepository.kt b/domain/home/src/main/java/com/terning/domain/home/repository/HomeRepository.kt index 609212b3c..dccccf145 100644 --- a/domain/home/src/main/java/com/terning/domain/home/repository/HomeRepository.kt +++ b/domain/home/src/main/java/com/terning/domain/home/repository/HomeRepository.kt @@ -1,16 +1,18 @@ package com.terning.domain.home.repository +import androidx.paging.PagingData import com.terning.domain.home.entity.ChangeFilteringRequestModel import com.terning.domain.home.entity.HomeFilteringInfo -import com.terning.domain.home.entity.HomeRecommendIntern +import com.terning.domain.home.entity.HomeRecommendedIntern import com.terning.domain.home.entity.HomeUpcomingIntern +import kotlinx.coroutines.flow.Flow interface HomeRepository { suspend fun getHomeUpcomingInternList(): Result - suspend fun getRecommendIntern( - sortBy: String, - ): Result + fun getRecommendIntern( + sortBy: String + ): Flow> suspend fun getFilteringInfo(): Result diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index e0b596600..b47898514 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -16,4 +16,8 @@ dependencies { // feature implementation(projects.feature.dialog) + + // paging + implementation(libs.paging.runtime) + implementation(libs.paging.compose) } \ No newline at end of file diff --git a/feature/home/src/main/java/com/terning/feature/home/HomeRoute.kt b/feature/home/src/main/java/com/terning/feature/home/HomeRoute.kt index eba443d2a..ba0a25c78 100644 --- a/feature/home/src/main/java/com/terning/feature/home/HomeRoute.kt +++ b/feature/home/src/main/java/com/terning/feature/home/HomeRoute.kt @@ -27,6 +27,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle +import androidx.paging.compose.collectAsLazyPagingItems import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.terning.core.analytics.EventType import com.terning.core.analytics.LocalTracker @@ -46,7 +47,7 @@ import com.terning.core.designsystem.theme.White import com.terning.core.designsystem.type.Grade import com.terning.core.designsystem.type.WorkingPeriod import com.terning.domain.home.entity.HomeFilteringInfo -import com.terning.domain.home.entity.HomeRecommendIntern +import com.terning.domain.home.entity.HomeRecommendedIntern import com.terning.domain.home.entity.HomeUpcomingIntern import com.terning.feature.dialog.cancel.ScrapCancelDialog import com.terning.feature.dialog.detail.ScrapDialog @@ -59,6 +60,7 @@ import com.terning.feature.home.component.HomeUpcomingInternScreen import okhttp3.internal.toImmutableList const val NAME_MAX_LENGTH = 5 +private const val ZERO_TOTAL_COUNT = 0 @Composable fun HomeRoute( @@ -85,8 +87,8 @@ fun HomeRoute( LaunchedEffect(key1 = true) { viewModel.getProfile() viewModel.getFilteringInfo() - viewModel.getRecommendInternsData(0) viewModel.getHomeUpcomingInternList() + viewModel.getRecommendInternFlow() } LaunchedEffect(viewModel.homeSideEffect, lifecycleOwner) { @@ -120,7 +122,7 @@ fun HomeRoute( updateSortingSheetVisibility = viewModel::updateSortingSheetVisibility, updateSortBy = viewModel::updateSortBy, getHomeUpcomingInternList = viewModel::getHomeUpcomingInternList, - getRecommendInternsData = viewModel::getRecommendInternsData, + updateInternModelScrapState = viewModel::updateInternScrapState, viewModel = viewModel, ) } @@ -135,10 +137,11 @@ fun HomeScreen( updateSortingSheetVisibility: (Boolean) -> Unit, updateSortBy: (Int) -> Unit, getHomeUpcomingInternList: () -> Unit, - getRecommendInternsData: (Int) -> Unit, + updateInternModelScrapState: () -> Unit, viewModel: HomeViewModel, ) { val homeState by viewModel.homeState.collectAsStateWithLifecycle() + val recommendedInternList = viewModel.recommendInternFlow.collectAsLazyPagingItems() val homeUserName = when (homeState.homeUserNameState) { is UiState.Success -> (homeState.homeUserNameState as UiState.Success).data @@ -150,14 +153,10 @@ fun HomeScreen( else -> HomeFilteringInfo(null, null, null, null) } - val homeRecommendInternList = when (homeState.homeRecommendInternState) { - is UiState.Success -> (homeState.homeRecommendInternState as UiState.Success).data.homeRecommendInternDetail.toImmutableList() - else -> listOf() - } - - val homeRecommendInternTotal = when (homeState.homeRecommendInternState) { - is UiState.Success -> (homeState.homeRecommendInternState as UiState.Success).data.totalCount - else -> 0 + val homeRecommendInternTotal = remember(recommendedInternList.loadState.refresh) { + if(recommendedInternList.itemCount > 0) { + recommendedInternList[0]?.totalCount ?: ZERO_TOTAL_COUNT + } else { ZERO_TOTAL_COUNT } } var changeFilteringSheetState by remember { mutableStateOf(false) } @@ -216,9 +215,7 @@ fun HomeScreen( updateRecommendDialogVisibility(false) if (isScrapCancelled) { getHomeUpcomingInternList() - getRecommendInternsData( - homeState.sortBy.ordinal, - ) + updateInternModelScrapState() } } ) @@ -235,9 +232,7 @@ fun HomeScreen( onDismissRequest = { isScrapped -> updateRecommendDialogVisibility(false) if (isScrapped) { - getRecommendInternsData( - homeState.sortBy.ordinal, - ) + updateInternModelScrapState() getHomeUpcomingInternList() } }, @@ -324,34 +319,8 @@ fun HomeScreen( } } - if (homeRecommendInternList.isNotEmpty()) { - items(homeRecommendInternList.size) { index -> - RecommendInternItem( - navigateToIntern = navigateToIntern, - intern = homeRecommendInternList[index], - onScrapButtonClicked = { - amplitudeTracker.track( - type = EventType.CLICK, - name = "home_scrap" - ) - updateRecommendDialogVisibility(true) - with(homeRecommendInternList[index]) { - viewModel.updateHomeInternModel( - internshipAnnouncementId = internshipAnnouncementId, - companyImage = companyImage, - title = title, - dDay = dDay, - deadline = deadline, - workingPeriod = workingPeriod, - isScrapped = isScrapped, - color = color, - startYearMonth = startYearMonth, - ) - } - } - ) - } - } else { + + if (recommendedInternList.itemCount == 0) { item { HomeRecommendEmptyIntern( text = @@ -359,6 +328,35 @@ fun HomeScreen( else R.string.home_recommend_no_intern ) } + } else { + items(recommendedInternList.itemCount, key = { it }) { index -> + recommendedInternList[index]?.run { + RecommendInternItem( + navigateToIntern = navigateToIntern, + intern = this, + onScrapButtonClicked = { + amplitudeTracker.track( + type = EventType.CLICK, + name = "home_scrap" + ) + updateRecommendDialogVisibility(true) + with(this) { + viewModel.updateHomeInternModel( + internshipAnnouncementId = internshipAnnouncementId, + companyImage = companyImage, + title = title, + dDay = dDay, + deadline = deadline, + workingPeriod = workingPeriod, + isScrapped = isScrapped, + color = color, + startYearMonth = startYearMonth, + ) + } + } + ) + } + } } } } @@ -367,7 +365,7 @@ fun HomeScreen( @Composable private fun RecommendInternItem( - intern: HomeRecommendIntern.HomeRecommendInternDetail, + intern: HomeRecommendedIntern, navigateToIntern: (Long) -> Unit, onScrapButtonClicked: (Long) -> Unit, ) { diff --git a/feature/home/src/main/java/com/terning/feature/home/HomeState.kt b/feature/home/src/main/java/com/terning/feature/home/HomeState.kt index 1f0ca01c8..f21fd5430 100644 --- a/feature/home/src/main/java/com/terning/feature/home/HomeState.kt +++ b/feature/home/src/main/java/com/terning/feature/home/HomeState.kt @@ -7,6 +7,7 @@ import com.terning.domain.home.entity.HomeRecommendIntern import com.terning.domain.home.entity.HomeUpcomingIntern data class HomeState( + val totalCount: Int = 0, val sortBy: SortBy = SortBy.EARLIEST, val sortingSheetVisibility: Boolean = false, val homeUserNameState: UiState = UiState.Loading, diff --git a/feature/home/src/main/java/com/terning/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/terning/feature/home/HomeViewModel.kt index 435f811de..ffb82a7ac 100644 --- a/feature/home/src/main/java/com/terning/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/terning/feature/home/HomeViewModel.kt @@ -2,17 +2,26 @@ package com.terning.feature.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.map import com.terning.core.designsystem.state.UiState import com.terning.core.designsystem.type.SortBy import com.terning.domain.home.entity.ChangeFilteringRequestModel import com.terning.domain.home.entity.HomeRecommendIntern +import com.terning.domain.home.entity.HomeRecommendedIntern import com.terning.domain.home.repository.HomeRepository import com.terning.domain.mypage.repository.MyPageRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -29,23 +38,41 @@ class HomeViewModel @Inject constructor( private val _homeSideEffect = MutableSharedFlow() val homeSideEffect get() = _homeSideEffect.asSharedFlow() - fun getRecommendInternsData(sortBy: Int) { - viewModelScope.launch { - homeRepository.getRecommendIntern( - sortBy = SortBy.entries[sortBy].type, - ).onSuccess { internList -> - _homeState.value = _homeState.value.copy( - homeRecommendInternState = UiState.Success(internList) - ) - }.onFailure { exception: Throwable -> - _homeState.value = _homeState.value.copy( - homeRecommendInternState = UiState.Failure(exception.toString()) - ) - _homeSideEffect.emit(HomeSideEffect.ShowToast(DesignSystemR.string.server_failure)) - } + private val scrapStateFlow: MutableStateFlow> = + MutableStateFlow(emptyMap()) + + private val _recommendInternFlow: MutableStateFlow>> = + MutableStateFlow(flow { }) + + @OptIn(ExperimentalCoroutinesApi::class) + val recommendInternFlow: Flow> = combine( + _recommendInternFlow.flatMapLatest { + it.cachedIn(viewModelScope) + }, scrapStateFlow + ) { paging, scrapState -> + paging.map { intern -> + val isScrapped = scrapState[intern.internshipAnnouncementId] ?: intern.isScrapped + intern.copy( + isScrapped = isScrapped + ) } } + fun getRecommendInternFlow() { + refreshScrapStateFlow() + refreshRecommendInternFlow() + } + + private fun refreshScrapStateFlow() { + scrapStateFlow.value = emptyMap() + } + + private fun refreshRecommendInternFlow() { + _recommendInternFlow.value = homeRepository.getRecommendIntern( + sortBy = _homeState.value.sortBy.type + ).cachedIn(viewModelScope) + } + fun getHomeUpcomingInternList() { viewModelScope.launch { homeRepository.getHomeUpcomingInternList().onSuccess { upcomingIntern -> @@ -87,7 +114,7 @@ class HomeViewModel @Inject constructor( ) ).onSuccess { getFilteringInfo() - getRecommendInternsData(_homeState.value.sortBy.ordinal) + refreshRecommendInternFlow() } } } @@ -147,9 +174,7 @@ class HomeViewModel @Inject constructor( sortBy = SortBy.entries[sortBy] ) } - getRecommendInternsData( - _homeState.value.sortBy.ordinal, - ) + refreshRecommendInternFlow() } fun updateSortingSheetVisibility(visibility: Boolean) { @@ -171,4 +196,14 @@ class HomeViewModel @Inject constructor( _homeSideEffect.emit(HomeSideEffect.NavigateToIntern(announcementId)) } } + + fun updateInternScrapState() { + _homeState.value.homeInternModel?.run { + val isScrapped = scrapStateFlow.value[this.internshipAnnouncementId] ?: this.isScrapped + + scrapStateFlow.update { currentMap -> + currentMap + (this.internshipAnnouncementId to !isScrapped) + } + } + } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c8db9f5c..fab201d8e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -95,6 +95,9 @@ dokka = "1.9.0" ## amplitude amplitude = "1.17.3" +## Paging +paging = "3.3.2" + [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } @@ -177,6 +180,10 @@ process-phoenix = { group = "com.jakewharton", name = "process-phoenix", version accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" } amplitude = { group = "com.amplitude", name = "analytics-android", version.ref = "amplitude" } +paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" } +paging-common = { group = "androidx.paging", name = "paging-common", version.ref = "paging" } +paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" } + [bundles] retrofit =[ "retrofit-core",