From 9cee2d7a5904eadebf535cbb33af1f36a9ac347f Mon Sep 17 00:00:00 2001 From: ando Date: Tue, 21 Feb 2023 18:46:54 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=A0Excepti?= =?UTF-8?q?on=E3=82=92throw=E3=81=99=E3=82=8B=E7=AE=87=E6=89=80=E3=82=92Da?= =?UTF-8?q?taSource=E3=81=A7=E8=A1=8C=E3=81=86=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../androidgithubsearch/data/api/GithubApi.kt | 44 ++++++++++--------- .../data/di/DataSourceModule.kt | 4 +- .../data/paging/GithubRepoPagingSource.kt | 6 +-- .../repository/GithubRepoRepositoryImpl.kt | 9 ++-- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/data/src/main/java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt b/data/src/main/java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt index ce53068..21f0e03 100644 --- a/data/src/main/java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt +++ b/data/src/main/java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt @@ -14,7 +14,7 @@ import io.ktor.http.* import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json -internal class GithubApi(private val format: Json) { +internal class GithubApi(private val format: Json, private val ktorHandler: KtorHandler) { private val httpClient: HttpClient by lazy { HttpClient(Android) { defaultRequest { @@ -44,34 +44,38 @@ internal class GithubApi(private val format: Json) { perPage: Int = SEARCH_PER_PAGE, sort: String = "stars" ): SearchRepositoryResponse { - /* - サーバーサイドのAPI開発が完了するまではFlavorをstubにし、開発を進める. - return format.decodeFromStubData( - context, - format, - "search_repositories_success.json" - ) - */ - val response: HttpResponse = httpClient.get { - url { path("search", "repositories") } - parameter("q", query) - parameter("page", page) - parameter("per_page", perPage) - parameter("sort", sort) + return ktorHandler.dataOrThrow { + /* + // サーバーサイドのAPI開発が完了するまではFlavorをstubにし、開発を進める. + format.decodeFromStubData( + context, + format, + "search_repositories_success.json" + ) + */ + val response: HttpResponse = httpClient.get { + url { path("search", "repositories") } + parameter("q", query) + parameter("page", page) + parameter("per_page", perPage) + parameter("sort", sort) + } + format.decodeFromString(response.body()) } - return format.decodeFromString(response.body()) } suspend fun fetchRepositoryDetail( ownerName: String, repositoryName: String ): RepositoryDetailResponse { - val response: HttpResponse = httpClient.get { - url { - path("repos", ownerName, repositoryName) + return ktorHandler.dataOrThrow { + val response: HttpResponse = httpClient.get { + url { + path("repos", ownerName, repositoryName) + } } + format.decodeFromString(response.body()) } - return format.decodeFromString(response.body()) } companion object { diff --git a/data/src/main/java/com/leoleo/androidgithubsearch/data/di/DataSourceModule.kt b/data/src/main/java/com/leoleo/androidgithubsearch/data/di/DataSourceModule.kt index 406fba1..243170b 100644 --- a/data/src/main/java/com/leoleo/androidgithubsearch/data/di/DataSourceModule.kt +++ b/data/src/main/java/com/leoleo/androidgithubsearch/data/di/DataSourceModule.kt @@ -1,6 +1,7 @@ package com.leoleo.androidgithubsearch.data.di import com.leoleo.androidgithubsearch.data.api.GithubApi +import com.leoleo.androidgithubsearch.data.api.KtorHandler import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -22,5 +23,6 @@ import javax.inject.Singleton internal object DataSourceModule { @Singleton @Provides - fun provideGithubService(format: Json): GithubApi = GithubApi(format) + fun provideGithubService(format: Json, ktorHandler: KtorHandler): GithubApi = + GithubApi(format, ktorHandler) } \ No newline at end of file diff --git a/data/src/main/java/com/leoleo/androidgithubsearch/data/paging/GithubRepoPagingSource.kt b/data/src/main/java/com/leoleo/androidgithubsearch/data/paging/GithubRepoPagingSource.kt index 7637d17..a2af139 100644 --- a/data/src/main/java/com/leoleo/androidgithubsearch/data/paging/GithubRepoPagingSource.kt +++ b/data/src/main/java/com/leoleo/androidgithubsearch/data/paging/GithubRepoPagingSource.kt @@ -12,7 +12,6 @@ import com.leoleo.androidgithubsearch.domain.model.RepositorySummary internal class GithubRepoPagingSource( private val query: String, private val api: GithubApi, - private val ktorHandler: KtorHandler, ) : PagingSource() { override fun getRefreshKey(state: PagingState): Int? { @@ -33,10 +32,7 @@ internal class GithubRepoPagingSource( val size = params.loadSize val from = pageNumber * size val placeholdersEnabled = params.placeholdersEnabled - val data = - ktorHandler.dataOrThrow { - api.searchRepositories(query = query, page = pageNumber).toModels() - } + val data = api.searchRepositories(query = query, page = pageNumber).toModels() // Since {INIT_PAGE_NO} is the lowest page number, return null to signify no more pages should // be loaded before it. val prevKey = if (pageNumber > INIT_PAGE_NO) pageNumber - 1 else null diff --git a/data/src/main/java/com/leoleo/androidgithubsearch/data/repository/GithubRepoRepositoryImpl.kt b/data/src/main/java/com/leoleo/androidgithubsearch/data/repository/GithubRepoRepositoryImpl.kt index 2093f5d..b79ad69 100644 --- a/data/src/main/java/com/leoleo/androidgithubsearch/data/repository/GithubRepoRepositoryImpl.kt +++ b/data/src/main/java/com/leoleo/androidgithubsearch/data/repository/GithubRepoRepositoryImpl.kt @@ -5,7 +5,6 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import com.leoleo.androidgithubsearch.data.api.GithubApi import com.leoleo.androidgithubsearch.data.api.GithubApi.Companion.SEARCH_PER_PAGE -import com.leoleo.androidgithubsearch.data.api.KtorHandler import com.leoleo.androidgithubsearch.data.api.response.toModel import com.leoleo.androidgithubsearch.data.paging.GithubRepoPagingSource import com.leoleo.androidgithubsearch.domain.model.RepositoryDetail @@ -15,15 +14,13 @@ import kotlinx.coroutines.flow.Flow import javax.inject.Inject internal class GithubRepoRepositoryImpl @Inject constructor( - private val api: GithubApi, - private val ktorHandler: KtorHandler, + private val api: GithubApi ) : GithubRepoRepository { override suspend fun getRepositoryDetail( ownerName: String, repositoryName: String - ): RepositoryDetail = - ktorHandler.dataOrThrow { api.fetchRepositoryDetail(ownerName, repositoryName).toModel() } + ): RepositoryDetail = api.fetchRepositoryDetail(ownerName, repositoryName).toModel() override fun searchRepositories(query: String): Flow> { return Pager( @@ -32,7 +29,7 @@ internal class GithubRepoRepositoryImpl @Inject constructor( initialLoadSize = SEARCH_PER_PAGE, enablePlaceholders = true ), - pagingSourceFactory = { GithubRepoPagingSource(query, api, ktorHandler) }, + pagingSourceFactory = { GithubRepoPagingSource(query, api) }, ).flow } } \ No newline at end of file From 035174661632940aa58fac2b3f12b25e06132d47 Mon Sep 17 00:00:00 2001 From: ando Date: Tue, 21 Feb 2023 19:22:53 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=8F?= =?UTF-8?q?=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=AFHttpRespo?= =?UTF-8?q?nseValidator=E3=82=92=E4=BD=BF=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../androidgithubsearch/data/api/GithubApi.kt | 69 ++++++++++++------- .../data/api/KtorHandler.kt | 58 +++++----------- .../data/di/NetworkModule.kt | 5 +- .../data/paging/GithubRepoPagingSource.kt | 1 - 4 files changed, 62 insertions(+), 71 deletions(-) diff --git a/data/src/main/java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt b/data/src/main/java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt index 21f0e03..6acfe8a 100644 --- a/data/src/main/java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt +++ b/data/src/main/java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt @@ -1,8 +1,10 @@ package com.leoleo.androidgithubsearch.data.api import com.leoleo.androidgithubsearch.data.BuildConfig +import com.leoleo.androidgithubsearch.data.api.response.GithubErrorResponse import com.leoleo.androidgithubsearch.data.api.response.RepositoryDetailResponse import com.leoleo.androidgithubsearch.data.api.response.SearchRepositoryResponse +import com.leoleo.androidgithubsearch.domain.exception.ApiErrorType import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.android.* @@ -34,7 +36,30 @@ internal class GithubApi(private val format: Json, private val ktorHandler: Ktor logger = AppHttpLogger() level = LogLevel.BODY } - expectSuccess = true + expectSuccess = true // HttpResponseValidatorで必要な設定. + HttpResponseValidator { + handleResponseExceptionWithRequest { e, _ -> + when (e) { + is ClientRequestException -> { // ktor: 400番台のエラー + val errorResponse = e.response + val message = + format.decodeFromString(errorResponse.body()).message + when (errorResponse.status) { + HttpStatusCode.Unauthorized -> throw ApiErrorType.UnAuthorized( + message + ) + HttpStatusCode.NotFound -> throw ApiErrorType.NotFound(message) + HttpStatusCode.Forbidden -> throw ApiErrorType.Forbidden(message) + HttpStatusCode.UnprocessableEntity -> { + throw ApiErrorType.UnprocessableEntity(message) + } + else -> throw ApiErrorType.Unknown(message) + } + } + else -> ktorHandler.handleResponseException(e) + } + } + } } } @@ -44,38 +69,34 @@ internal class GithubApi(private val format: Json, private val ktorHandler: Ktor perPage: Int = SEARCH_PER_PAGE, sort: String = "stars" ): SearchRepositoryResponse { - return ktorHandler.dataOrThrow { - /* - // サーバーサイドのAPI開発が完了するまではFlavorをstubにし、開発を進める. - format.decodeFromStubData( - context, - format, - "search_repositories_success.json" - ) - */ - val response: HttpResponse = httpClient.get { - url { path("search", "repositories") } - parameter("q", query) - parameter("page", page) - parameter("per_page", perPage) - parameter("sort", sort) - } - format.decodeFromString(response.body()) + /* + // サーバーサイドのAPI開発が完了するまではFlavorをstubにし、開発を進める. + return format.decodeFromStubData( + context, + format, + "search_repositories_success.json" + ) + */ + val response: HttpResponse = httpClient.get { + url { path("search", "repositories") } + parameter("q", query) + parameter("page", page) + parameter("per_page", perPage) + parameter("sort", sort) } + return format.decodeFromString(response.body()) } suspend fun fetchRepositoryDetail( ownerName: String, repositoryName: String ): RepositoryDetailResponse { - return ktorHandler.dataOrThrow { - val response: HttpResponse = httpClient.get { - url { - path("repos", ownerName, repositoryName) - } + val response: HttpResponse = httpClient.get { + url { + path("repos", ownerName, repositoryName) } - format.decodeFromString(response.body()) } + return format.decodeFromString(response.body()) } companion object { diff --git a/data/src/main/java/com/leoleo/androidgithubsearch/data/api/KtorHandler.kt b/data/src/main/java/com/leoleo/androidgithubsearch/data/api/KtorHandler.kt index 5ae4c69..bfa1730 100644 --- a/data/src/main/java/com/leoleo/androidgithubsearch/data/api/KtorHandler.kt +++ b/data/src/main/java/com/leoleo/androidgithubsearch/data/api/KtorHandler.kt @@ -1,55 +1,29 @@ package com.leoleo.androidgithubsearch.data.api -import com.leoleo.androidgithubsearch.data.api.response.GithubErrorResponse import com.leoleo.androidgithubsearch.domain.exception.ApiErrorType import io.ktor.client.call.* import io.ktor.client.network.sockets.* import io.ktor.client.plugins.* import io.ktor.http.* -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.withContext -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json import java.net.UnknownHostException -internal class KtorHandler( - private val dispatcher: CoroutineDispatcher, - private val format: Json, -) { - suspend fun dataOrThrow(apiCall: suspend () -> T): T { - return withContext(dispatcher) { - try { - apiCall.invoke() - } catch (e: Throwable) { - when (e) { - is UnknownHostException, is HttpRequestTimeoutException, is ConnectTimeoutException, is SocketTimeoutException -> { - throw ApiErrorType.Network - } - // ktor: 300番台のエラー - is RedirectResponseException -> throw ApiErrorType.Redirect - is ClientRequestException -> { // ktor: 400番台のエラー - val errorResponse = e.response - val message = - format.decodeFromString(errorResponse.body()).message - when (errorResponse.status) { - HttpStatusCode.Unauthorized -> throw ApiErrorType.UnAuthorized( - message - ) - HttpStatusCode.NotFound -> throw ApiErrorType.NotFound(message) - HttpStatusCode.Forbidden -> throw ApiErrorType.Forbidden(message) - HttpStatusCode.UnprocessableEntity -> { - throw ApiErrorType.UnprocessableEntity(message) - } - else -> throw ApiErrorType.Unknown(message) - } - } - // ktor: 500番台のエラー - is ServerResponseException -> throw ApiErrorType.Server - // ktor: それ以外のエラー - is ResponseException -> throw ApiErrorType.Unknown(e.localizedMessage) - else -> throw ApiErrorType.Unknown(e.localizedMessage) - } +internal class KtorHandler { + /** + * 共通のAPI エラーハンドリングはここで行う. + */ + @Throws(ApiErrorType::class) + fun handleResponseException(e: Throwable) { + when (e) { + is UnknownHostException, is HttpRequestTimeoutException, is ConnectTimeoutException, is SocketTimeoutException -> { + throw ApiErrorType.Network } + // ktor: 300番台のエラー + is RedirectResponseException -> throw ApiErrorType.Redirect + // ktor: 500番台のエラー + is ServerResponseException -> throw ApiErrorType.Server + // ktor: それ以外のエラー + is ResponseException -> throw ApiErrorType.Unknown(e.localizedMessage) + else -> throw ApiErrorType.Unknown(e.localizedMessage) } } } \ No newline at end of file diff --git a/data/src/main/java/com/leoleo/androidgithubsearch/data/di/NetworkModule.kt b/data/src/main/java/com/leoleo/androidgithubsearch/data/di/NetworkModule.kt index ef3c56b..2e263ea 100644 --- a/data/src/main/java/com/leoleo/androidgithubsearch/data/di/NetworkModule.kt +++ b/data/src/main/java/com/leoleo/androidgithubsearch/data/di/NetworkModule.kt @@ -19,8 +19,5 @@ internal object NetworkModule { @Singleton @Provides - fun provideKtorHandler( - @IoDispatcher dispatcher: CoroutineDispatcher, - format: Json - ): KtorHandler = KtorHandler(dispatcher, format) + fun provideKtorHandler(): KtorHandler = KtorHandler() } \ No newline at end of file diff --git a/data/src/main/java/com/leoleo/androidgithubsearch/data/paging/GithubRepoPagingSource.kt b/data/src/main/java/com/leoleo/androidgithubsearch/data/paging/GithubRepoPagingSource.kt index a2af139..ffc90da 100644 --- a/data/src/main/java/com/leoleo/androidgithubsearch/data/paging/GithubRepoPagingSource.kt +++ b/data/src/main/java/com/leoleo/androidgithubsearch/data/paging/GithubRepoPagingSource.kt @@ -4,7 +4,6 @@ import android.util.Log import androidx.paging.PagingSource import androidx.paging.PagingState import com.leoleo.androidgithubsearch.data.api.GithubApi -import com.leoleo.androidgithubsearch.data.api.KtorHandler import com.leoleo.androidgithubsearch.data.api.response.toModels import com.leoleo.androidgithubsearch.domain.exception.ValidationErrorType import com.leoleo.androidgithubsearch.domain.model.RepositorySummary From 1dfe1be60bf45c437460b9aab9ec54668f4af598 Mon Sep 17 00:00:00 2001 From: ando Date: Tue, 21 Feb 2023 19:25:49 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=E9=81=A9=E5=88=87=E3=81=AA=E7=AE=87?= =?UTF-8?q?=E6=89=80=E3=81=AB@Throws=E3=82=92=E3=81=A4=E3=81=91=E3=82=8B?= =?UTF-8?q?=20#84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/src/main/java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt b/data/src/main/java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt index 6acfe8a..dc567de 100644 --- a/data/src/main/java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt +++ b/data/src/main/java/com/leoleo/androidgithubsearch/data/api/GithubApi.kt @@ -63,6 +63,7 @@ internal class GithubApi(private val format: Json, private val ktorHandler: Ktor } } + @kotlin.jvm.Throws(ApiErrorType::class) suspend fun searchRepositories( query: String, page: Int, @@ -87,6 +88,7 @@ internal class GithubApi(private val format: Json, private val ktorHandler: Ktor return format.decodeFromString(response.body()) } + @kotlin.jvm.Throws(ApiErrorType::class) suspend fun fetchRepositoryDetail( ownerName: String, repositoryName: String