Skip to content

Commit

Permalink
#187 Retrofit2에 CustomCallAdapter를 추가하여 오류 처리를 일관적 으로 한곳에서 처리하도록 개선
Browse files Browse the repository at this point in the history
  • Loading branch information
pknujsp committed Aug 10, 2023
1 parent 8ab478b commit a4f9e31
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 18 deletions.
5 changes: 4 additions & 1 deletion core/network/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id("mediproject.android.feature")
id("kotlinx-serialization")
alias(libs.plugins.kapt)
alias(libs.plugins.ksp)
}

android {
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ class NetworkStatusManager @Inject constructor(@ApplicationContext private val c
}

Lifecycle.Event.ON_PAUSE -> {

}

else -> {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,8 +21,7 @@ class GoogleSearchDataSourceImpl @Inject constructor(
private val urlCache = LruCache<String, String>(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<String> {
val finalQuery = (additionalQuery + query).safetyEncode()
Expand All @@ -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)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<String>
): NetworkApiResult<String>
}

private const val ENCODING: String = "utf8"
Expand Down
Original file line number Diff line number Diff line change
@@ -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<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
if (Call::class.java != getRawType(returnType))
return null


check(returnType is ParameterizedType) {
"반환 유형은 Call<NetworkApiResult<<Foo>> or Call<NetworkApiResult<out Foo>>로 매개변수화 되어야 합니다."
}

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<Foo> or NetworkApiResult<out Foo>로 매개변수화 되어야 합니다." }

val successBodyType = getParameterUpperBound(0, responseType)

return NetworkApiCallAdapter<Any>(successBodyType)
}
}

internal class NetworkApiCallAdapter<R>(
private val successType: Type,
) : CallAdapter<R, Call<NetworkApiResult<R>>> {
override fun adapt(call: Call<R>): Call<NetworkApiResult<R>> = NetworkApiCall(call, successType)

override fun responseType(): Type = successType
}


internal class NetworkApiCall<R>(
private val delegate: Call<R>,
private val successType: Type,
) : Call<NetworkApiResult<R>> {

override fun enqueue(callback: Callback<NetworkApiResult<R>>) = delegate.enqueue(
object : Callback<R> {

override fun onResponse(call: Call<R>, response: Response<R>) {
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<R?>, 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<NetworkApiResult<R>> {
throw UnsupportedOperationException("NetworkApiResult doesn't support execute")
}

override fun request(): Request = delegate.request()
override fun timeout(): Timeout = delegate.timeout()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.android.mediproject.core.network.retrofitext

import io.github.pknujsp.core.annotation.KBindFunc

@KBindFunc
sealed interface NetworkApiResult<out T> {
data class Success<out T>(val data: T) : NetworkApiResult<T>

sealed class Failure(open val exception: Throwable) : NetworkApiResult<Nothing> {
class ApiError(exception: Throwable) : Failure(exception)
class NetworkError(exception: Throwable) : Failure(exception)
class UnknownError(exception: Throwable) : Failure(exception)
}
}

0 comments on commit a4f9e31

Please sign in to comment.