Skip to content

Commit

Permalink
[#17] 통신을 위한 네트워크 관련 베이스 코드 작성
Browse files Browse the repository at this point in the history
- 초기 베이스 코드
  • Loading branch information
heechokim committed Mar 31, 2022
1 parent ec0f3ee commit dbfcc41
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 0 deletions.
8 changes: 8 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,15 @@ 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'

// retrofit2
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

// coroutine
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
}
45 changes: 45 additions & 0 deletions app/src/main/java/com/moyerun/moyeorun_android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,55 @@ package com.moyerun.moyeorun_android

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.moyerun.moyeorun_android.common.Lg
import com.moyerun.moyeorun_android.network.ApiErrorCode
import com.moyerun.moyeorun_android.network.callAdapter.NetworkResponse
import com.moyerun.moyeorun_android.network.client.api
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

apiCallTest()
}

private fun apiCallTest() {
GlobalScope.launch {
val response1 = api.getUserRepoList("choheeis")
when (response1) {
is NetworkResponse.Success -> {
Lg.d("getUserRepoList Success") }
is NetworkResponse.ApiError -> {
// 비즈니스 로직 상의 에러
// 이넘 클래스로 별도 관리하는 에러 코드와 비교하여 알맞은 로직 작성
val errorCode = response1.code // 이건 상태코드이긴 한데, error 안에 코드 넣어서 보내주는 것 사용.
if (errorCode == ApiErrorCode.NOT_EXIST_USER.code) {
// 에러 대응
}
Lg.d("getUserRepoList ApiError")
}
is NetworkResponse.NetworkError -> {
// 네트워크 에러와 언논 에러는 전처리되면 좋을 듯
// Success랑 API Error만 뷰 모델에서 관리하도록.
Lg.d("getUserRepoList NetworkError")
}
is NetworkResponse.UnknownError -> { Lg.d("getUserRepoList UnknownError") }
}

val response2 = api.getAppName()
when (response2) {
is NetworkResponse.Success -> { Lg.d("getAppName Success") }
is NetworkResponse.ApiError -> {
// 비즈니스 로직 상의 에러
// 이넘 클래스로 별도 관리하는 에러 코드와 비교하여 알맞은 로직 작성
Lg.d("getAppName ApiError")
}
is NetworkResponse.NetworkError -> { Lg.d("getAppName NetworkError") }
is NetworkResponse.UnknownError -> { Lg.d("getAppName UnknownError") }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.moyerun.moyeorun_android.network

enum class ApiErrorCode(val code: Int) {
NOT_EXIST_USER(900)
}
14 changes: 14 additions & 0 deletions app/src/main/java/com/moyerun/moyeorun_android/network/Response.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.moyerun.moyeorun_android.network

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

data class Error<T>(
val message: String,
val error: T
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.moyerun.moyeorun_android.network.api

import com.moyerun.moyeorun_android.network.Error
import com.moyerun.moyeorun_android.network.Success
import com.moyerun.moyeorun_android.network.callAdapter.NetworkResponse
import retrofit2.http.GET
import retrofit2.http.Path

interface ApiService {
@GET("users/{user}/repos")
suspend fun getUserRepoList(@Path("user") user: String): NetworkResponse<Success<Any>, Error<Any>>

@GET("appName")
suspend fun getAppName(): NetworkResponse<Success<Any>, Error<Any>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.moyerun.moyeorun_android.network.callAdapter

import java.io.IOException

sealed class NetworkResponse<out T : Any,out U : Any> {
data class Success<T : Any>(val body: T) : NetworkResponse<T, Nothing>()
data class ApiError<U : Any>(val body: U, val code: Int) : NetworkResponse<Nothing, U>()

This comment has been minimized.

Copy link
@heechokim

heechokim Apr 2, 2022

Author Collaborator

에러 3가지로 나눌 필요가 있는지?
Failure 안에서 분기 처리 해도 됨.

data class NetworkError(val error: IOException) : NetworkResponse<Nothing, Nothing>()
data class UnknownError(val error: Throwable?) : NetworkResponse<Nothing, Nothing>()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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 NetworkResponseAdapterFactory : CallAdapter.Factory() {

This comment has been minimized.

Copy link
@heechokim

heechokim Apr 2, 2022

Author Collaborator

디자인 패턴 Factory 패턴.


override fun get(

This comment has been minimized.

Copy link
@heechokim

heechokim Apr 2, 2022

Author Collaborator

이 부분 굉장히 중요. (정확히 이해하자)

returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {

// suspend functions wrap the response type in `Call`
if (Call::class.java != getRawType(returnType)) {
return null
}

// check first that the return type is `ParameterizedType`
check(returnType is ParameterizedType) {
"return type must be parameterized as Call<Result<Foo>> or Call<Result<out Foo>>"
}

// 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) != NetworkResponse::class.java) {
return null
}

// the response type is ApiResponse and should be parameterized
check(responseType is ParameterizedType) {
"Response must be parameterized as Result<Foo> or Result<out Foo>"
}

val successBodyType = getParameterUpperBound(0, responseType)
val errorBodyType = getParameterUpperBound(1, responseType)

val errorBodyConverter = retrofit.nextResponseBodyConverter<Any>(null, errorBodyType, annotations)

return NetworkResponseCallAdapter<Any, Any>(successBodyType, errorBodyConverter)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.moyerun.moyeorun_android.network.callAdapter

import okhttp3.Request
import okhttp3.ResponseBody
import okio.Timeout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Converter
import retrofit2.Response
import java.io.IOException
import java.lang.Exception
import java.lang.UnsupportedOperationException

internal class NetworkResponseCall<S : Any, E : Any>(
private val delegate: Call<S>,
private val errorConverter: Converter<ResponseBody, E>
) : Call<NetworkResponse<S, E>> {

override fun enqueue(callback: Callback<NetworkResponse<S, E>>) {
return delegate.enqueue(object : Callback<S> {
override fun onResponse(call: Call<S>, response: Response<S>) {
val body = response.body()
val code = response.code()
val error = response.errorBody()

if (response.isSuccessful) {
// status code가 200번대 일 때
if (body != null) {
callback.onResponse(
this@NetworkResponseCall,
Response.success(NetworkResponse.Success(body))
)
} else {
// body 없을 때 별도 에러 만들어도 될 듯.
callback.onResponse(
this@NetworkResponseCall,
Response.success(NetworkResponse.UnknownError(null))
)
}
} else {
val errorBody = when {
error == null -> null
error.contentLength() == 0L -> null
else -> try {
errorConverter.convert(error)
} catch (e: Exception) {
null
}
}

if (errorBody != null) {
callback.onResponse(
this@NetworkResponseCall,
Response.success(NetworkResponse.ApiError(errorBody, code))
)
} else {
callback.onResponse(
this@NetworkResponseCall,
Response.success(NetworkResponse.UnknownError(null))
)
}
}
}

override fun onFailure(call: Call<S>, t: Throwable) {
val networkResponse = when (t) {
is IOException -> NetworkResponse.NetworkError(t)
else -> NetworkResponse.UnknownError(t)
}
// 여기서 전처리 하면 됨.
callback.onResponse(
this@NetworkResponseCall,
Response.success(networkResponse)
)
}
})
}

override fun clone(): Call<NetworkResponse<S, E>> = NetworkResponseCall(delegate.clone(), errorConverter)

override fun execute(): Response<NetworkResponse<S, E>> {
throw UnsupportedOperationException("NetworkResponseCall 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,16 @@
package com.moyerun.moyeorun_android.network.callAdapter

import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Converter
import java.lang.reflect.Type

class NetworkResponseCallAdapter<S : Any, E : Any>(
private val successType: Type,
private val errorBodyConverter: Converter<ResponseBody, E>
) : CallAdapter<S, Call<NetworkResponse<S, E>>> {
override fun responseType(): Type = successType

override fun adapt(call: Call<S>): Call<NetworkResponse<S, E>> = NetworkResponseCall(call, errorBodyConverter)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.moyerun.moyeorun_android.network.client

import com.moyerun.moyeorun_android.network.callAdapter.NetworkResponseAdapterFactory
import com.moyerun.moyeorun_android.network.api.ApiService
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

private const val BASE_URL = "https://api.github.com/"

private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addCallAdapterFactory(NetworkResponseAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build()

val api: ApiService = retrofit.create(ApiService::class.java)

0 comments on commit dbfcc41

Please sign in to comment.