diff --git a/app/build.gradle b/app/build.gradle index 9d935bc..1fbd7e2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,6 +18,10 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + def localProperties = new Properties() + localProperties.load(rootProject.file('./local.properties').newDataInputStream()) + buildConfigField("String", "BASE_URL", localProperties['baseUrl']) } signingConfigs { @@ -68,6 +72,7 @@ android { dependencies { def glide_version = '4.13.1' + def retrofit_version = '2.9.0' implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.1' @@ -77,6 +82,7 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + // Firebase implementation platform('com.google.firebase:firebase-bom:29.2.0') implementation 'com.google.firebase:firebase-analytics-ktx' implementation 'com.google.firebase:firebase-crashlytics-ktx' @@ -88,4 +94,11 @@ dependencies { // Glide implementation "com.github.bumptech.glide:glide:$glide_version" annotationProcessor "com.github.bumptech.glide:compiler:$glide_version" + + // Retrofit2 + implementation "com.squareup.retrofit2:retrofit:$retrofit_version" + implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" + + // Coroutine + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 76e7c02..19ebc43 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ - + + { TODO() } + else -> ApiException(url, error) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiResponse.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiResponse.kt new file mode 100644 index 0000000..a1efc94 --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiResponse.kt @@ -0,0 +1,13 @@ +package com.moyerun.moyeorun_android.network.api + +/** + * 서버 팀과 합의한 성공/실패 응답 값에 대한 모델입니다. + */ +data class Success( + val data: T +) + +data class Error( + val message: String?, + val case: String +) \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiService.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiService.kt new file mode 100644 index 0000000..9b3d4dc --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiService.kt @@ -0,0 +1,5 @@ +package com.moyerun.moyeorun_android.network.api + +interface ApiService { + +} \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResult.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResult.kt new file mode 100644 index 0000000..e21e8cf --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResult.kt @@ -0,0 +1,10 @@ +package com.moyerun.moyeorun_android.network.calladapter + +/** + * Success : API 호출 성공 시, body를 Wrapping 합니다. + * Failure : API 호출 실패 시, throwable을 Wrpping 합니다. + */ +sealed class ApiResult { + data class Success(val body: T) : ApiResult() + data class Failure(val throwable: Throwable) : ApiResult() +} \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResultCall.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResultCall.kt new file mode 100644 index 0000000..a53f34f --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResultCall.kt @@ -0,0 +1,107 @@ +package com.moyerun.moyeorun_android.network.calladapter + +import com.google.gson.Gson +import com.moyerun.moyeorun_android.common.exceptions.NetworkException +import com.moyerun.moyeorun_android.network.api.ApiErrorCase +import com.moyerun.moyeorun_android.network.api.Error +import okhttp3.Request +import okio.Timeout +import retrofit2.* + +class ApiResultCall( + private val delegate: Call +) : Call> { + override fun enqueue(callback: Callback>) { + delegate.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val requestUrl = delegate.request().url().toString() + + // status code 200번대. + if (response.isSuccessful) { + val body = response.body() + if (body != null) { + callback.onResponse( + this@ApiResultCall, + Response.success(ApiResult.Success(body)) + ) + } else { + callback.onResponse( + this@ApiResultCall, + Response.success( + ApiResult.Failure( + NetworkException( + "Response from $requestUrl was null " + + "but response body type was decleared as non-null", + HttpException(response) + ) + ) + ) + ) + } + } else { // status code 200번대 아님. + val errorBody = response.errorBody() + if (errorBody != null) { + val errorResponse = Gson().fromJson(errorBody.string(), Error::class.java) + callback.onResponse( + this@ApiResultCall, + Response.success( + ApiResult.Failure( + ApiErrorCase.getException( + requestUrl, + errorResponse, + HttpException(response) + ) + ) + ) + ) + } else { + callback.onResponse( + this@ApiResultCall, + Response.success( + ApiResult.Failure( + NetworkException( + requestUrl, + HttpException(response) + ) + ) + ) + ) + } + } + } + + override fun onFailure(call: Call, throwable: Throwable) { + callback.onResponse( + this@ApiResultCall, + Response.success( + ApiResult.Failure( + NetworkException( + call.request().url().toString(), + throwable + ) + ) + ) + ) + } + }) + } + + override fun clone(): Call> = ApiResultCall(delegate.clone()) + + override fun execute(): Response> { + throw UnsupportedOperationException("ApiResultCall doesn't support execute") + } + + override fun isExecuted(): Boolean = delegate.isExecuted + + override fun cancel() { + delegate.cancel() + } + + override fun isCanceled(): Boolean = delegate.isCanceled + + override fun request(): Request = delegate.request() + + override fun timeout(): Timeout = delegate.timeout() + +} \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResultCallAdapter.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResultCallAdapter.kt new file mode 100644 index 0000000..3924227 --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResultCallAdapter.kt @@ -0,0 +1,13 @@ +package com.moyerun.moyeorun_android.network.calladapter + +import retrofit2.Call +import retrofit2.CallAdapter +import java.lang.reflect.Type + +class ApiResultCallAdapter( + private val returnType: Type +) : CallAdapter>> { + override fun responseType(): Type = returnType + + override fun adapt(call: Call): Call> = ApiResultCall(call) +} \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResultCallAdapterFactory.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResultCallAdapterFactory.kt new file mode 100644 index 0000000..081174c --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResultCallAdapterFactory.kt @@ -0,0 +1,44 @@ +package com.moyerun.moyeorun_android.network.calladapter + +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.Retrofit +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class ApiResultCallAdapterFactory : CallAdapter.Factory() { + + override fun get( + returnType: Type, + annotations: Array, + retrofit: Retrofit + ): CallAdapter<*, *>? { + + // suspend functions wrap the response type in `Call` + if (getRawType(returnType) != Call::class.java) { + return null + } + + // check first that the return type is `ParameterizedType` + if (returnType !is ParameterizedType) { + return null + } + + // get the response type inside the `Call` type + val responseType = getParameterUpperBound(0, returnType) + + // if the response type is not ApiResponse then we can't handle this type, so we return null + if (getRawType(responseType) != ApiResult::class.java) { + return null + } + + // the response type is ApiResponse and should be parameterized + if (responseType !is ParameterizedType) { + return null + } + + val successResponseType = getParameterUpperBound(0, responseType) + + return ApiResultCallAdapter(successResponseType) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/client/Retrofit.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/client/Retrofit.kt new file mode 100644 index 0000000..1a1775b --- /dev/null +++ b/app/src/main/java/com/moyerun/moyeorun_android/network/client/Retrofit.kt @@ -0,0 +1,17 @@ +package com.moyerun.moyeorun_android.network.client + +import com.moyerun.moyeorun_android.BuildConfig +import com.moyerun.moyeorun_android.network.calladapter.ApiResultCallAdapterFactory +import com.moyerun.moyeorun_android.network.api.ApiService +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +private const val BASE_URL = BuildConfig.BASE_URL + +val retrofit: Retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .addCallAdapterFactory(ApiResultCallAdapterFactory()) + .addConverterFactory(GsonConverterFactory.create()) + .build() + +val apiService: ApiService = retrofit.create(ApiService::class.java) \ No newline at end of file