-
Notifications
You must be signed in to change notification settings - Fork 1
feat : 작품 검색 연결 및 Paging 데이터 계층 구현 #947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8bd7f27
a3e392b
9e36cde
5ed8c99
2f7fbf3
21f62c0
592c049
9cf8a68
d17aa74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import com.into.websoso.setNamespace | ||
|
|
||
| plugins { | ||
| id("websoso.android.library") | ||
| alias(libs.plugins.kotlin.serialization) | ||
| } | ||
|
|
||
| android { | ||
| setNamespace("data.novel") | ||
| } | ||
|
|
||
| dependencies { | ||
| implementation(projects.core.network) | ||
|
|
||
| implementation(libs.paging.runtime) | ||
| implementation(libs.retrofit) | ||
| implementation(libs.serialization.json) | ||
|
|
||
| testImplementation(libs.junit) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest /> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.into.websoso.data.novel | ||
|
|
||
| import retrofit2.http.GET | ||
| import retrofit2.http.Query | ||
|
|
||
| internal interface NovelSearchApi { | ||
| @GET("novels") | ||
| suspend fun getNovels( | ||
| @Query("query") query: String, | ||
| @Query("page") page: Int, | ||
| @Query("size") size: Int, | ||
| ): NovelSearchResponseDto | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.into.websoso.data.novel | ||
|
|
||
| import dagger.Module | ||
| import dagger.Provides | ||
| import dagger.hilt.InstallIn | ||
| import dagger.hilt.components.SingletonComponent | ||
| import retrofit2.Retrofit | ||
| import javax.inject.Singleton | ||
|
|
||
| @Module | ||
| @InstallIn(SingletonComponent::class) | ||
| internal object NovelSearchApiModule { | ||
| @Provides | ||
| @Singleton | ||
| fun provideNovelSearchApi(retrofit: Retrofit): NovelSearchApi = retrofit.create(NovelSearchApi::class.java) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package com.into.websoso.data.novel | ||
|
|
||
| import androidx.paging.PagingSource | ||
| import androidx.paging.PagingState | ||
| import com.into.websoso.data.novel.model.NovelSearchEntity | ||
|
|
||
| internal class NovelSearchPagingSource( | ||
| private val query: String, | ||
| private val api: NovelSearchApi, | ||
| ) : PagingSource<Int, NovelSearchEntity>() { | ||
| override suspend fun load(params: LoadParams<Int>): LoadResult<Int, NovelSearchEntity> = | ||
| runCatching { | ||
| val page = params.key ?: INITIAL_PAGE | ||
| val response = api.getNovels( | ||
| query = query, | ||
| page = page, | ||
| size = params.loadSize, | ||
| ) | ||
|
|
||
| LoadResult.Page( | ||
| data = response.novels.map(NovelSearchResponseDto.NovelDto::toData), | ||
| prevKey = page.takeIf { it > INITIAL_PAGE }?.minus(1), | ||
| nextKey = (page + 1).takeIf { response.isLoadable }, | ||
| ) | ||
| }.getOrElse(LoadResult<Int, NovelSearchEntity>::Error) | ||
|
Comment on lines
+11
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
file='data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt'
printf '%s\n' '--- file ---'
cat -n "$file"
printf '%s\n' '--- related declarations/usages ---'
rg -n --glob '*.kt' 'class NovelSearchPagingSource|NovelSearchPagingSource|override suspend fun load|runCatching' data/novel data 2>/dev/null | head -200
printf '%s\n' '--- project Kotlin/coroutines configuration ---'
rg -n --glob 'build.gradle*' --glob '*.gradle.kts' --glob 'libs.versions.toml' 'kotlin|coroutines|paging' . | head -200Repository: Team-WSS/WSS-Android Length of output: 10260 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
p = Path('data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt')
text = p.read_text()
print('runCatching present:', 'runCatching' in text)
print('CancellationException handling present:', 'CancellationException' in text)
print('LoadResult.Error present:', 'LoadResult.Error' in text)
PYRepository: Team-WSS/WSS-Android Length of output: 262 코루틴 취소를
🤖 Prompt for AI Agents |
||
|
|
||
| override fun getRefreshKey(state: PagingState<Int, NovelSearchEntity>): Int? = | ||
| state.anchorPosition?.let { anchorPosition -> | ||
| state.closestPageToPosition(anchorPosition)?.let { page -> | ||
| page.prevKey?.plus(1) ?: page.nextKey?.minus(1) | ||
| } | ||
| } | ||
|
|
||
| private companion object { | ||
| const val INITIAL_PAGE = 0 | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.into.websoso.data.novel | ||
|
|
||
| import com.into.websoso.data.novel.model.NovelSearchEntity | ||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| internal data class NovelSearchResponseDto( | ||
| @SerialName("isLoadable") | ||
| val isLoadable: Boolean, | ||
| @SerialName("novels") | ||
| val novels: List<NovelDto>, | ||
| ) { | ||
| @Serializable | ||
| data class NovelDto( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 전부 nullable 처리가능하게끔 하는게 좋을것같습니다! |
||
| @SerialName("novelId") | ||
| val novelId: Long, | ||
| @SerialName("title") | ||
| val title: String, | ||
| @SerialName("author") | ||
| val author: String, | ||
| @SerialName("novelImage") | ||
| val imageUrl: String, | ||
| ) { | ||
| fun toData(): NovelSearchEntity = | ||
| NovelSearchEntity( | ||
| novelId = novelId, | ||
| title = title, | ||
| author = author, | ||
| imageUrl = imageUrl, | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.into.websoso.data.novel.model | ||
|
|
||
| data class NovelSearchEntity( | ||
| val novelId: Long, | ||
| val title: String, | ||
| val author: String, | ||
| val imageUrl: String, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ include( | |
| include( | ||
| ":data:account", | ||
| ":data:library", | ||
| ":data:novel", | ||
| ":data:feed", | ||
| ) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: Team-WSS/WSS-Android
Length of output: 50377
🏁 Script executed:
Repository: Team-WSS/WSS-Android
Length of output: 50377
🏁 Script executed:
Repository: Team-WSS/WSS-Android
Length of output: 19425
🏁 Script executed:
Repository: Team-WSS/WSS-Android
Length of output: 50377
🏁 Script executed:
Repository: Team-WSS/WSS-Android
Length of output: 4927
🌐 Web query:
AndroidX PagingSource load CancellationException runCatching Kotlin official documentation💡 Result:
When implementing the
loadmethod in an AndroidXPagingSource, it is critical to handleCancellationExceptioncorrectly because it is used by the Kotlin Coroutines framework to signal that a coroutine has been cancelled [1][2]. Using the standard libraryrunCatchingblock inside a suspendingloadfunction is generally discouraged [3].runCatchingcatches allThrowableexceptions, includingCancellationException, and encapsulates them in aResult.failureobject [4][3]. If aCancellationExceptionis caught and swallowed byrunCatchinginstead of being re-thrown, it can prevent the coroutine from cancelling properly, potentially breaking structured concurrency and causing the Paging library or other parts of your app to continue executing work that should have been terminated [5][3]. To handle errors properly within aPagingSource.loadmethod: 1. Avoid wrapping the entireloadlogic in a genericrunCatchingblock [3]. 2. Catch specific, expected exceptions (such asIOExceptionfor network errors orHttpExceptionfor API errors) and return them asLoadResult.Error[6][7][8]. 3. Ensure thatCancellationExceptionis allowed to propagate up the call stack [2][3]. If you must catch it, you must re-throw it [5][2]. Example of correct error handling inPagingSource.load: override suspend fun load(params: LoadParams): LoadResult<Key, Value> { return try { // Perform network or database operations val data = api.fetchData(params.key) LoadResult.Page(data = data,...) } catch (e: Exception) { // Rethrow CancellationException so the coroutine can cancel if (e is CancellationException) throw e // Handle expected errors and return them as LoadResult.Error LoadResult.Error(e) } } If you frequently need to catch exceptions in coroutines while preserving cancellation, you can create a custom helper function (sometimes referred to assuspendCatching) that explicitly re-throwsCancellationExceptionbefore catching otherThrowabletypes [3].Citations:
runCatchingthat does not handle aCancellationExceptionbut re-throws it instead. Kotlin/kotlinx.coroutines#1814🌐 Web query:
Kotlin runCatching catches Throwable CancellationException official documentation💡 Result:
Yes, the standard library function runCatching catches CancellationException [1][2]. According to the official Kotlin documentation and its implementation, runCatching is designed to catch any Throwable [3]. Because CancellationException is a subclass of Throwable (it inherits from IllegalStateException [4]), it is caught and encapsulated as a failure in the resulting Result object [3][1]. This behavior is significant in the context of Kotlin Coroutines, which rely on CancellationException to signal and propagate cancellation for structured concurrency [5][6][7]. When runCatching intercepts this exception, it effectively swallows the cancellation signal, which can prevent a coroutine from terminating as expected when its parent scope is cancelled [8][1][2]. To maintain correct coroutine behavior, it is recommended to avoid using runCatching with suspending code or to use a custom implementation that explicitly rethrows CancellationException [9][1][2]. Example of a safe variant that rethrows CancellationException: public inline suspend fun runSuspendCatching(block: -> R): Result { return try { Result.success(block) } catch (c: CancellationException) { throw c } catch (e: Throwable) { Result.failure(e) } } [1]
Citations:
runCatchingthat does not handle aCancellationExceptionbut re-throws it instead. Kotlin/kotlinx.coroutines#1814🏁 Script executed:
Repository: Team-WSS/WSS-Android
Length of output: 317
CancellationException을 전파하세요.runCatching은CancellationException도LoadResult.Error로 변환할 수 있습니다.load가 취소되면 해당 예외를 다시 던지고, 예상된 API 예외만LoadResult.Error로 변환하세요.GET /novels의page·size계산 규칙도 확인하세요. 서버가page * size로 오프셋을 계산하면params.loadSize와page + 1조합으로 항목이 중복되거나 누락될 수 있습니다.🤖 Prompt for AI Agents