본 게시글은 저의 학습을 위한 용도라 부족한 점이 많을 수 있어요.
만약 틀린 부분이 있다면 따뜻한 조언과 피드백 부탁드립니다:) 🧚
Api를 연결할 때 retrofit2를 자주 사용한다.
제대로 알고 연결한 적이 없어서 이번 기회를 통해서 정리를 확실히 해두려 한다.
참고
build.gradle
에 retrofit 라이브러리를 추가한다.
retorofit github에 가서 최신 버전을 확인!!
implementation 'com.squareup.retrofit2:retrofit:(insert latest version)'
Manifiest
에 인터넷 권한설정을 해준다.
<uses-permission android:name="android.permission.INTERNET" />
object MobalRetrofit {
const val API_END_POINT = "http://"
fun <T> create(
service: Class<T>,
client: OkHttpClient,
httpUrl: String = API_END_POINT
): T = Retrofit.Builder()
.baseUrl(httpUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.build()
.create(service)
inline fun <reified T : Any> create(
client: OkHttpClient,
httpUrl: String = API_END_POINT
): T {
require(httpUrl.isNotBlank()) { "Parameter httpUrl cannot be blank." }
return create(service = T::class.java, httpUrl = httpUrl, client = client)
}
}
함께 개발하는 팀원이 위 과정을 만들어놔서 나는 아래만 진행하면 됐다.
내가 지금 진행하고 있는 프로젝트에서 사용하는 DTO를 예시로 들자면, POST Dto를 만든다.
이 프로젝트에서는 폴더 구조를 (프로젝트 패키지) -> data -> dto -> PostDto.kt
이렇게 가져갔다.
여기서 간단하게 Dto 란?
Data Transfer Object 로서 계층간 데이터 교환을 위한 객체이다. DB에서 데이터를 얻어 Service나 Controller 등으터 보낼 때 사용하는 객체를 말한다.
serialized
를 사용하는 것을
data class PostDto(
@SerializedName("post_id") val postId: Int,
@SerializedName("user_id") val userId: Int,
@SerializedName("post_image") val postImage: String? = null,
@SerializedName("title") val title: String,
@SerializedName("post_description") val description: String? = null,
@SerializedName("goal") val goalPrice: Int,
@SerializedName("created_at") val createdAt: Long? = null,
@SerializedName("update_at") val updatedAt: Long? = null,
@SerializedName("started_at") val startedAt: Long,
@SerializedName("end_at") val endAt: Long
)
본 프로젝트에서는 api interface를 Service라고 네이밍 하였다.
Post를 만들어서 보내야하므로, POST방식
으로 보내게 되고, 내부 body에는 RequestBody형식을 넣어주어서 함수를 사용할 때 body를 정의해준다. 폴더 구조는 (프로젝트 패키지) > network > service > CreateDonationService.kt
였다.
interface CreateDonationService {
@POST("/posts")
fun createDonation(@Body body: RequestBody): Single<Response<PostDto>>
}
이 프로젝트의 경우 Rx를 사용하고 있어서, response또한 Single형식으로 받도록 지정했다. Response는 아래 코드와 같이 정의되어 있다.
data class Response<T>(
val code: Int? = null,
val data: T? = null,
val message: String? = null
)
@CheckReturnValue
@SchedulerSupport(SchedulerSupport.NONE)
fun <T : Any> Single<Response<T>>.onErrorResponse(tag: String? = null): Single<Response<T>> =
onErrorResumeNext { t -> t.toErrorResponse<T>(tag)?.let { Single.just(it) } ?: Single.error(t) }
위에서 정의한 service를 인자로 받고, createDonation함수를 정의한다. 함수의 인자론, Dto에 정의된 데이터들을 인자로 받는다.
내부에서는 service에서 정의한 함수를 body를 정의하여 api call을 보낸다. 그러면 반환되는것은 single 형식의 response가 올 것이다.
class CreateDonationRepository @Inject constructor(private val service: CreateDonationService) {
fun createDonation(
title: String,
description: String?,
postImage: String?,
goal: Int,
startedAt: Long,
endAt: Long
): Single<Response<PostDto>> {
return service.createDonation(
requestBodyOf {
"title" to title
description?.let { "post_description" to it }
postImage?.let { "post_image" to it }
"goal" to goal
"startedAt" to startedAt
"endAt" to endAt
}
)
}
}
createDonationRepository을 생성자에 주입하고, 아래서 사용합니다.
class CreateDonationViewModel @Inject constructor(
schedulerProvider: BaseSchedulerProvider,
private val createDonationRepository: CreateDonationRepository,
private val fileRepository: FileRepository
) : BaseViewModel(schedulerProvider) {
...
// 함수들
fun createDonation(context: Context) {
_createDonationInputSubject.firstOrError()
.subscribeOnIO()
.flatMap { createDonationInput ->
// null이 아니라면
if (!createDonationInput.description.isNullOrBlank() &&
...
// input에 들어있는 값 null체크.
) {
fileRepository.uploadImage(context, createDonationInput.postImage).flatMap {
createDonationRepository.createDonation(
title = createDonationInput.productName,
description = createDonationInput.description,
postImage = it,
goal = createDonationInput.fundAmount,
startedAt = createDonationInput.startDate,
endAt = createDonationInput.dueDate
)
}
// null이라면
} else {
Single.error(
IllegalArgumentException(
"create donation failed: createDonationInput: $createDonationInput"
)
)
}
}
// response로 받아와서 완료 데이터 생성.
.subscribeWithErrorLogger { response ->
if (response.data != null) {
_createCompleteInputSubject.onNext(
// 완료 데이터 발행
)
} else {
response.message?.let { _createDonationErrorMessageSubject.onNext(it) }
}
}
.addToDisposables()
}
사실 Hilt, Dagger, Rx, 다 생소하기도 하고, 앞 두 개는 아예 모르는 부분이라서 코드 설명을 하기가 좀 어렵다...
일반적으로 나와있는 예시 보다 더 뎁스가 들어가서 모듈화 되어있는 부분도 코드가 이해안되는 것에 한 몫하는 것 같다.
이 프로젝트가 끝나고, 꼭 코드를 복기하는 과정을 거쳐야겠다고 생각했다.