Skip to content

Commit

Permalink
Merge pull request #33 from MoyeoRun/feature/network-base
Browse files Browse the repository at this point in the history
[#17] 통신을 위한 네트워크 관련 베이스 코드 작성
  • Loading branch information
heechokim authored May 11, 2022
2 parents edd691c + f76c118 commit d87d9e3
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 1 deletion.
13 changes: 13 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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'
Expand All @@ -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'
Expand All @@ -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'
}
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.moyerun.moyeorun_android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.INTERNET"/>

<application
android:name=".MoyeoRunApplication"
android:allowBackup="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.moyerun.moyeorun_android.common.exceptions

import com.moyerun.moyeorun_android.network.api.Error

class ApiException(val url: String, val error: Error) : RuntimeException() {
val case: String
get() = error.case

override val message: String?
get() = error.message
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.moyerun.moyeorun_android.common.exceptions

import java.lang.RuntimeException

class NetworkException(message: String, cause: Throwable) : RuntimeException(message, cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.moyerun.moyeorun_android.network.api

import com.moyerun.moyeorun_android.common.exceptions.ApiException

enum class ApiErrorCase(val case: String) {
// 서버와 합의된 에러 케이스들을 정의합니다.
NOT_LOGIN("100"), // TODO : 예시입니다. 실제 로그인 작업 시 수정해주세요.
UNKNOWN("999");

companion object {
private fun findError(case: String): ApiErrorCase {
return values().find { it.case == case } ?: UNKNOWN
}

fun getException(url: String, error: Error, cause: Throwable? = null): Throwable {
return when(findError(error.case)) {
NOT_LOGIN -> { TODO() }
else -> ApiException(url, error)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.moyerun.moyeorun_android.network.api

/**
* 서버 팀과 합의한 성공/실패 응답 값에 대한 모델입니다.
*/
data class Success<T>(
val data: T
)

data class Error(
val message: String?,
val case: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.moyerun.moyeorun_android.network.api

interface ApiService {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.moyerun.moyeorun_android.network.calladapter

/**
* Success : API 호출 성공 시, body를 Wrapping 합니다.
* Failure : API 호출 실패 시, throwable을 Wrpping 합니다.
*/
sealed class ApiResult<out T> {
data class Success<T>(val body: T) : ApiResult<T>()
data class Failure(val throwable: Throwable) : ApiResult<Nothing>()
}
Original file line number Diff line number Diff line change
@@ -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<S>(
private val delegate: Call<S>
) : Call<ApiResult<S>> {
override fun enqueue(callback: Callback<ApiResult<S>>) {
delegate.enqueue(object : Callback<S> {
override fun onResponse(call: Call<S>, response: Response<S>) {
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<S>, throwable: Throwable) {
callback.onResponse(
this@ApiResultCall,
Response.success(
ApiResult.Failure(
NetworkException(
call.request().url().toString(),
throwable
)
)
)
)
}
})
}

override fun clone(): Call<ApiResult<S>> = ApiResultCall(delegate.clone())

override fun execute(): Response<ApiResult<S>> {
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()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.moyerun.moyeorun_android.network.calladapter

import retrofit2.Call
import retrofit2.CallAdapter
import java.lang.reflect.Type

class ApiResultCallAdapter<T>(
private val returnType: Type
) : CallAdapter<T, Call<ApiResult<T>>> {
override fun responseType(): Type = returnType

override fun adapt(call: Call<T>): Call<ApiResult<T>> = ApiResultCall(call)
}
Original file line number Diff line number Diff line change
@@ -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<out Annotation>,
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<Any>(successResponseType)
}
}
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit d87d9e3

Please sign in to comment.