Retrofit: Rest API 통신을 위한 OkHttp라이브러리의 상위 구현체
API: 프로그램들이 서로 통신하기 위한 매개체(함수, 데이터 등을 주고 받을 수 있다.)
REST: 자원을 이름으로 구분하여 해당 자원의 상태를 주고 받는 모든 것
REST API란 보다 복잡하고 원칙이 많은 개념이다.
모듈 분리: 개발은 지속적인 유지보수가 필요하다. 한 모듈의 모든 소스코드가 중구난방하게 배치되어 있다면 무언가를 수정해야할 때 매우 불편할 것이다. 이를 위해 클린아키텍처가 필요하며 이는 보다 복잡한 개념이므로 다음에 알아보고
우선은 data, presentation으로 개념적으로 소스코드를 구분하여 모듈과 패키지를 분리하여 구현하여보자.
우선 data라는 이름으로 모듈을 생성한다.
data모듈에서 api 패키지를 생성해준다.(di는 일단 무시하자)
app 모듈에선 UI와 app의 생명주기(Activity)를 위한 소스를 모아놓고
data 모듈에선 app 모듈에서 사용될 데이터를 위한 소스들이 모여있다.
// build:gradle(Modlue :data)
implementation 'com.squareup.retrofit2:retrofit:2.6.4'
implementation 'com.squareup.retrofit2:retrofit-gson:2.6.4'
그냥 Json타입의 데이터를 호출한다면 데이터를 사용하기위해 어떤 형태로든 변환해주는 것이 데이터를 통제하는데 유리하다.
Json데이터가 아니더라도 어떤 데이터를 불러오는지 그 데이터의 타입에 변환이 필요한지 데이터 타입의 변환을 위해 유용한 라이브러리가 있는지 찾고 고려하는 것이 중요하다.
// build:gradle(Modlue: app)
implementation project(:data)1
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
....
....
...
{
"Name": "홍길동",
"age": 18,
}
data class ApiResponse{
@SerializedName("Name") val name : String,
val age: Int, // @SerializedName이 없다면 변수명이 일치하여야 한다.
}
interface ApiService{
@GET("{name}/")
suspend fun getData(@Path("name") currency: String): Call<ApiResponse>
//만약 im.sexy.guy/jung에서 데이터를 호출할 수 있으니 주소에서 @Path를 사용해
//호출 주소에 변수를 넣을 수 있게한다. 필요없다면 변수 없이 함수 선언하고 GET안에
//필요한 변수를 직접 넣어주면된다.
}
suspend란 코루틴스코프롤 통한 작업 처리중 언제든지 일시중지할 수있도록 하는 함수이다. suspend를 붙이지 않아도 됩니다. 필요애 따라서 fun getApi로 교체해도 좋습니다.
Object RetrofitModule{//
fun proviedRetrofit(): Retrofit{
return Retrofit.builder()
.baseUrl("https://im.sexy.guy/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
fun provideApiService(retrofit: Retrofit): ApiService{
return retrofit.create(ApiService::class.java)
}
fun provideApiServiceManager(ApiService: ApiService): ApiServiceManager {
return ApiServiceManager(ApiServiceManager)
}
}
원래 의존성 주입을 위해선 라이브러리를 사용하는 것이 좋다.
'3. 의존성 주입'에서 다룰 것이다.
class ApiServiceManager(private val apiService: ApiService) {
// API 호출 메서드 예시
suspend fun fetchData(name: String): ApiResponse? {
try {
val response: Response<ApiResponse> = apiService.getData(name)
if (response.isSuccessful) {
return response.body()
} else {
// API 호출이 실패한 경우 처리할 내용을 여기에 추가할 수 있습니다.
return null
}
} catch (e: Exception) {
// 네트워크 오류 등 예외 발생 시 처리할 내용을 여기에 추가할 수 있습니다.
return null
}
}
}
//build.gradle(Project:example)
plugins{
```
```
id 'com.google.dagger.hilt.android' version '2.44' apply false
}
//build.gradle(module:app)
plugins{
kotlin("kapt")
id "com.google.dagger.hilt.android"
...
}
dependencies{
//hilt
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-android-compiler:2.44"
}
//build.gradle(module:data)
plugins{
kotlin("kapt")
id "com.google.dagger.hilt.android"
...
}
dependencies{
//hilt
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-android-compiler:2.44"
}
간혹가다가 sync now후에 빌드가 안되는 경우가 있다.
이는 build.gradle(module:name)에서 compileOption과 kotlinOption의 자바버전과 jvm버전이 위의 kapt와 호환되지않는 경우이다. 필자의 경우에는 1.8 -> 17로 버전을 바꾸고 해결되었지만 혹시 1.8이 꼭 필요한 경우 방법을 잘 찾길 바란다.
app 모듈에서 사용할 ApiServiceManager와 RetrofitModule을 변경한다.
ApiServiceManager
class ApiServiceManager @Inject constructor (
private val songApiService: SongApiService
){
@Module
@InstallIn(SingletoneComponent::class)
Object RetrofitModule{
@Provides
fun proviedRetrofit(): Retrofit{
return Retrofit.builder()
.baseUrl("https://im.sexy.guy/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
fun provideApiService(retrofit: Retrofit): ApiService{
return retrofit.create(ApiService::class.java)
}
@Provides
fun provideApiServiceManager(ApiService: ApiService): ApiServiceManager {
return ApiServiceManager(ApiService)
}
}
MVVM 패턴을 위해 app 모듈에 viewmodel과 view 패키지를 각각 생성한다.
MVVM 패턴을 사용하여 가져온 데이터를 보여주는데 가져온 데이터를 Viewmodel에서 처리하여 view에서 보여주면 구조적으로도 깔끔하고 데이터를 통제하기에도 훨씬 편하다.
단, viwemodel을 사용하기 위해 app모듈에서 lifecycle 등에 관련된 것을 implementation이 필요할 수 있다. 이 또한 내용이 방대함으로 생략하고 필요한 것을 찾아서 implementation하시길 바랍니다.
ex) build.gradle.kts(module:app)val lifecycle_versions = "2.4.1" //lifecycle implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_versions") // kotlin을 위한 viewmodel implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_versions") // compose를 위한 implementation ("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_versions") // 실시간 데이터 관리를 위한 ```
viewmodel/ExampleViewModel
@HiltViewModel
class ExampleViewModel @Inject constructor(
private val apiServiceManager: ApiServiceManager
) : ViewModel() {
fun sampleDataLog(name: String){
val data = apiServiceManager.getData(name)
Log.d("Sample", "$data")
}
}
@AndroidEntryPoint
class MainActivity : ComponentActivity(){
@Inject
lateinit var apiServiceManager: ApiServiceManager
private lateinit var viewModel: ExampleViewModel
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
viewModel = ExampleViewModel(apiServiceManager)
var name: String = "hong"
viewModel.sampleDataLog(name)
}
}
내용이 작성되지 않아도 꼭 필요한 구성요소이다.
@HiltAndroidApp
class MyApplication : Application() {
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".MyApplication"
...
}
1) url 주소의 '/'가 올바른 위치에 있는지
2) 데이터구조에 맞게 interface와 data class를 제대로 적용되었는지(
{
"squadName": "Super hero squad",
"homeTown": "Metro City",
"formed": 2016,
"secretBase": "Super tower",
"active": true,
"members": [
{
"name": "Molecule Man",
"age": 29,
"secretIdentity": "Dan Jukes",
"powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
},
{
"name": "Madame Uppercut",
"age": 39,
"secretIdentity": "Jane Wilson",
"powers": [
"Million tonne punch",
"Damage resistance",
"Superhuman reflexes"
]
},
...
}
이런 식의 구조라면 '데이터 클래스를 하나 더 사용한다거나','getData' 함수 뒤의 Call<List>하는 등의 추가 작업이 필요하다.
3) (Call, Callback), (Response,Result) 등 Retrofit관련 다양한 호출 객체들이 있다 이가 맞는지도
4) RetrofitModule의 객체가 사용될 때 Singleton으로 사용되어야 하는지 고민 했는가
5) Retrofit은 비동기 처리 라이브러리이다. 비동기 작업에서의 로직 오류는 없는치 체트
6) Kotlin과 Gradle 버전
7) dagger-hilt는 최근 변동이 많은 라이브러리이다. 사라진 함수가 있을 수도 있고 새로 생긴 함수가 있을 수도 있다.