From a4f9e3154336c2921a2fd60f7b67f080e7395e94 Mon Sep 17 00:00:00 2001 From: JSPark <48265129+pknujsp@users.noreply.github.com> Date: Fri, 11 Aug 2023 00:12:50 +0900 Subject: [PATCH] =?UTF-8?q?#187=20Retrofit2=EC=97=90=20CustomCallAdapter?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9D=BC=EA=B4=80?= =?UTF-8?q?=EC=A0=81=20=EC=9C=BC=EB=A1=9C=20=ED=95=9C=EA=B3=B3=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/network/build.gradle.kts | 5 +- .../core/network/NetworkStatusManager.kt | 1 - .../image/GoogleSearchDataSourceImpl.kt | 26 ++--- .../network/module/GoogleSearchNetwork.kt | 9 +- .../NetworkApiCallAdapterFactory.kt | 107 ++++++++++++++++++ .../network/retrofitext/NetworkApiResult.kt | 14 +++ 6 files changed, 144 insertions(+), 18 deletions(-) create mode 100644 core/network/src/main/java/com/android/mediproject/core/network/retrofitext/NetworkApiCallAdapterFactory.kt create mode 100644 core/network/src/main/java/com/android/mediproject/core/network/retrofitext/NetworkApiResult.kt diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index c362799e6..6f2e68a9c 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("mediproject.android.feature") id("kotlinx-serialization") alias(libs.plugins.kapt) + alias(libs.plugins.ksp) } android { @@ -27,10 +28,12 @@ dependencies { implementation(libs.bundles.retrofits) implementation(libs.bundles.dataStores) implementation(libs.google.protobuf.kotlin.lite) - //implementation(libs.kotlinx.datetime) implementation(libs.okhttp.logginginterceptor) implementation(libs.okhttp) implementation(libs.jsoup) kapt(libs.androidx.hilt.compilerKapt) kapt(libs.androidx.hilt.work.compilerKapt) + + ksp(libs.ksealedbinding.compiler) + implementation(libs.ksealedbinding.annotation) } diff --git a/core/network/src/main/java/com/android/mediproject/core/network/NetworkStatusManager.kt b/core/network/src/main/java/com/android/mediproject/core/network/NetworkStatusManager.kt index 826357736..fa4ad8b61 100644 --- a/core/network/src/main/java/com/android/mediproject/core/network/NetworkStatusManager.kt +++ b/core/network/src/main/java/com/android/mediproject/core/network/NetworkStatusManager.kt @@ -56,7 +56,6 @@ class NetworkStatusManager @Inject constructor(@ApplicationContext private val c } Lifecycle.Event.ON_PAUSE -> { - } else -> {} diff --git a/core/network/src/main/java/com/android/mediproject/core/network/datasource/image/GoogleSearchDataSourceImpl.kt b/core/network/src/main/java/com/android/mediproject/core/network/datasource/image/GoogleSearchDataSourceImpl.kt index 455b18449..59f67936b 100644 --- a/core/network/src/main/java/com/android/mediproject/core/network/datasource/image/GoogleSearchDataSourceImpl.kt +++ b/core/network/src/main/java/com/android/mediproject/core/network/datasource/image/GoogleSearchDataSourceImpl.kt @@ -3,8 +3,8 @@ package com.android.mediproject.core.network.datasource.image import android.util.LruCache import com.android.mediproject.core.network.module.GoogleSearchNetworkApi import com.android.mediproject.core.network.module.safetyEncode -import com.android.mediproject.core.network.onResponse import com.android.mediproject.core.network.parser.HtmlParser +import com.android.mediproject.core.network.retrofitext.NetworkApiResult import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.newFixedThreadPoolContext @@ -21,8 +21,7 @@ class GoogleSearchDataSourceImpl @Inject constructor( private val urlCache = LruCache(100) private val mutex = Mutex() - @OptIn(DelicateCoroutinesApi::class) - private val threads = newFixedThreadPoolContext(3, "GoogleSearchProcessor") + @OptIn(DelicateCoroutinesApi::class) private val threads = newFixedThreadPoolContext(3, "GoogleSearchProcessor") private suspend fun getImageUrl(query: String, additionalQuery: String): Result { val finalQuery = (additionalQuery + query).safetyEncode() @@ -32,18 +31,19 @@ class GoogleSearchDataSourceImpl @Inject constructor( }?.run { Result.success(this) } ?: run { - googleSearchNetworkApi.getImageUrl(finalQuery).onResponse().fold( - onSuccess = { - val url = htmlParser.parse(finalQuery, it) + when (val result = googleSearchNetworkApi.getImageUrl(query = finalQuery)) { + is NetworkApiResult.Success -> { + val imageUrl = htmlParser.parse(finalQuery, result.data) mutex.withLock { - urlCache.put(finalQuery, url) + urlCache.put(finalQuery, imageUrl) } - Result.success(url) - }, - onFailure = { - Result.failure(it) - }, - ) + Result.success(imageUrl) + } + + is NetworkApiResult.Failure -> { + Result.failure(result.exception) + } + } } } diff --git a/core/network/src/main/java/com/android/mediproject/core/network/module/GoogleSearchNetwork.kt b/core/network/src/main/java/com/android/mediproject/core/network/module/GoogleSearchNetwork.kt index f216c318c..0bd60cdb2 100644 --- a/core/network/src/main/java/com/android/mediproject/core/network/module/GoogleSearchNetwork.kt +++ b/core/network/src/main/java/com/android/mediproject/core/network/module/GoogleSearchNetwork.kt @@ -3,12 +3,13 @@ package com.android.mediproject.core.network.module import com.android.mediproject.core.network.datasource.image.GoogleSearchDataSource import com.android.mediproject.core.network.datasource.image.GoogleSearchDataSourceImpl import com.android.mediproject.core.network.parser.HtmlParser +import com.android.mediproject.core.network.retrofitext.NetworkApiCallAdapterFactory +import com.android.mediproject.core.network.retrofitext.NetworkApiResult import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient -import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.scalars.ScalarsConverterFactory import retrofit2.http.GET @@ -25,7 +26,9 @@ object GoogleSearchNetwork { @Provides @Singleton fun providesGoogleSearchNetworkApi(@Named("okHttpClientWithoutAny") okHttpClient: OkHttpClient): GoogleSearchNetworkApi = - Retrofit.Builder().client(okHttpClient).addConverterFactory(ScalarsConverterFactory.create()).baseUrl(BASE_URL).build() + Retrofit.Builder().client(okHttpClient).addConverterFactory(ScalarsConverterFactory.create()) + .addCallAdapterFactory(NetworkApiCallAdapterFactory()) + .baseUrl(BASE_URL).build() .create(GoogleSearchNetworkApi::class.java) @Provides @@ -43,7 +46,7 @@ interface GoogleSearchNetworkApi { @Query("tbm", encoded = true) tbm: String = ISCH, @Query("ie", encoded = true) ie: String = ENCODING, @Query("oe", encoded = true) oe: String = ENCODING, - ): Response + ): NetworkApiResult } private const val ENCODING: String = "utf8" diff --git a/core/network/src/main/java/com/android/mediproject/core/network/retrofitext/NetworkApiCallAdapterFactory.kt b/core/network/src/main/java/com/android/mediproject/core/network/retrofitext/NetworkApiCallAdapterFactory.kt new file mode 100644 index 000000000..e7e93d2d5 --- /dev/null +++ b/core/network/src/main/java/com/android/mediproject/core/network/retrofitext/NetworkApiCallAdapterFactory.kt @@ -0,0 +1,107 @@ +package com.android.mediproject.core.network.retrofitext + +import okhttp3.Request +import okio.Timeout +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.Callback +import retrofit2.Response +import retrofit2.Retrofit +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +internal class NetworkApiCallAdapterFactory : CallAdapter.Factory() { + + override fun get(returnType: Type, annotations: Array, retrofit: Retrofit): CallAdapter<*, *>? { + if (Call::class.java != getRawType(returnType)) + return null + + + check(returnType is ParameterizedType) { + "반환 유형은 Call> or Call>로 매개변수화 되어야 합니다." + } + + val responseType = getParameterUpperBound(0, returnType) + if (getRawType(responseType) != NetworkApiResult::class.java) + return null + + // the response type is ApiResponse and should be parameterized + check(responseType is ParameterizedType) { "응답은 NetworkApiResult or NetworkApiResult로 매개변수화 되어야 합니다." } + + val successBodyType = getParameterUpperBound(0, responseType) + + return NetworkApiCallAdapter(successBodyType) + } +} + +internal class NetworkApiCallAdapter( + private val successType: Type, +) : CallAdapter>> { + override fun adapt(call: Call): Call> = NetworkApiCall(call, successType) + + override fun responseType(): Type = successType +} + + +internal class NetworkApiCall( + private val delegate: Call, + private val successType: Type, +) : Call> { + + override fun enqueue(callback: Callback>) = delegate.enqueue( + object : Callback { + + override fun onResponse(call: Call, response: Response) { + val isSuccessful = response.isSuccessful + val errorBody = response.errorBody() + + if (!isSuccessful) { + // errorBody가 null인 경우 -> UnknownError + // 아닌 경우 -> NetworkError + val throwable = Throwable(errorBody?.string() ?: "네트워크 응답 중 알수 없는 오류가 발생하였습니다!") + return callback.onResponse( + this@NetworkApiCall, + Response.success( + errorBody?.let { NetworkApiResult.Failure.ApiError(throwable) } + ?: NetworkApiResult.Failure.UnknownError(throwable), + ), + ) + } + + // body가 null인 경우 -> ApiError + // 아닌 경우 -> Success + return callback.onResponse( + this@NetworkApiCall, + Response.success( + response.body()?.let { NetworkApiResult.Success(it) } + ?: NetworkApiResult.Failure.UnknownError( + IllegalStateException( + "네트워크 응답 결과 body가 null입니다!", + ), + ), + ), + ) + } + + override fun onFailure(call: Call, throwable: Throwable) { + callback.onResponse(this@NetworkApiCall, Response.success(NetworkApiResult.Failure.NetworkError(throwable))) + } + }, + ) + + override fun isExecuted() = delegate.isExecuted + + override fun clone() = NetworkApiCall(delegate.clone(), successType) + + override fun isCanceled() = delegate.isCanceled + + override fun cancel() = delegate.cancel() + + override fun execute(): Response> { + throw UnsupportedOperationException("NetworkApiResult doesn't support execute") + } + + override fun request(): Request = delegate.request() + override fun timeout(): Timeout = delegate.timeout() + +} diff --git a/core/network/src/main/java/com/android/mediproject/core/network/retrofitext/NetworkApiResult.kt b/core/network/src/main/java/com/android/mediproject/core/network/retrofitext/NetworkApiResult.kt new file mode 100644 index 000000000..2122f045e --- /dev/null +++ b/core/network/src/main/java/com/android/mediproject/core/network/retrofitext/NetworkApiResult.kt @@ -0,0 +1,14 @@ +package com.android.mediproject.core.network.retrofitext + +import io.github.pknujsp.core.annotation.KBindFunc + +@KBindFunc +sealed interface NetworkApiResult { + data class Success(val data: T) : NetworkApiResult + + sealed class Failure(open val exception: Throwable) : NetworkApiResult { + class ApiError(exception: Throwable) : Failure(exception) + class NetworkError(exception: Throwable) : Failure(exception) + class UnknownError(exception: Throwable) : Failure(exception) + } +}