Programming/Android

RxJava + Retrofit 으로 통신 모듈 구현하기

YK Choi 2021. 10. 3. 23:45

RxJava3와 Retrofit2 를 사용하여 통신 모듈을 만들어보자

추가로 여기서 DI는 hilt를 사용했다.

 

api는 ITunes Api를 사용하겠다. 샘플 코드로 적당한 api인 것 같다.

 

만들 클래스는 아래와 같다.(당장 api 통신을 하기에는 약간의 부가적인 것도 있지만 확장성을 생각해서 전부 설명하겠다.)

 

- ITunesService

- ITunesClient

- NetworkModule

 

ITunesService 이다. 

반환형이 Single으로 둔 이유는 RxJava의 Observable중 Single이 한 번의 이벤트에서 사용될때 쓰이기 때문이다.

참고로 나는 여기서 한번 @Query의 어노테이션을 깜박하고 빼먹었는데 

`No Retrofit annotation found. (parameter #1)` 의 런타임에러가 발생하니 다음번엔 조심하자!

interface ITunesService {
    @GET("/search?")
    fun fetchTrackList(
        @Query("term") term:String,
        @Query("entity") entity:String,
        @Query("limit") limit: Int,
        @Query("offset") offset:Int
    ): Single<TrackListResponse>
}

 

ITunesClient 이다.

class ITunesClient @Inject constructor(
    private val iTunesService: ITunesService
) {
    fun fetchTrackList(
        term: String,
        entity: String,
        limit: Int,
        offset: Int
    ): Single<TrackListResponse> = iTunesService.fetchTrackList(term, entity, limit, offset)
}

 

 

NetworkModule 이다.

OKHttp와 Retrofit을 provide해주고 ITunesService와 ITunesClient를 생성해준다.

 

참고로 나는 provideRetrofit()쪽에서 addCallAdapterFactory()를 빼먹은 적이 있는데

`Could not locate call adapter for io.reactivex.rxjava3.core.Single...` 와 같은 런타임 에러가 발생한다.

addCallAdapterFactory()를 추가해도 같은 에러가 계속 발생했다...;;

이유는 build.gradle(모듈)에서 dependency를 추가할 때

`implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofit_version"` 가 rxJava3이 아니라 2로 되어있었기 때문이었다. 실제 RxJava 버전과 같은 것인지 반드시 확인하자.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    const val BASE_URL = "https://itunes.apple.com/"

    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor().apply {
                level = if (BuildConfig.DEBUG) {
                    HttpLoggingInterceptor.Level.BODY
                } else {
                    HttpLoggingInterceptor.Level.NONE
                }
            })
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .client(okHttpClient)
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideITunesService(retrofit: Retrofit): ITunesService {
        return retrofit.create(ITunesService::class.java)
    }

    @Provides
    @Singleton
    fun provideITunesClient(iTunesService: ITunesService): ITunesClient{
        return ITunesClient(iTunesService)
    }
}

 

이제 사용하려는 repository나 viewModel 쪽에서 호출하면 된다.

아래의 경우처럼 viewModel에서 repository를 통해 호출할 땐 아래 코드처럼 observeOn, subscribeOn, subscribe()등과 같이 적절한 연산자를 이용해서 처리하면 된다. 아래에서 쓴 연산자는 일부의 연산자이니 적절한 연산자를 조사하고 적재적소에 찾아서 쓰는 연습이 필요하다.

 

disposable.add(
    repository.fetchTrackList(
        term = "greenday",
        entity = "song",
        limit = 20,
        offset = 0
    ).observeOn(AndroidSchedulers.mainThread())
        .subscribeOn(Schedulers.io())
        .subscribe({
            Timber.e("fetchTrackList success ${it.results}")
        }, {
        Timber.e("fetchTrackList fail")
        })
)

 

주의

위 코드의 disposable에 집중해보자.

저것은 전역변수로 선언한

private val disposable = CompositeDisposable()

이다.

 

그리고 ViewModel에 onCleared() 될때 아래와 같이 dispose 시켜야한다.

마치 coroutine이 ViewModelScope{} 이 뷰모델의 lifecycle에서 동작하도록 하는 것이다.

이를 제대로 처리하지 않으면 앱을 종료해도 사용이 끝난 리소스를 반납하지 않아 메모리릭이 발생할 수 있다.

    override fun onCleared() {
        super.onCleared()

        if (!disposable.isDisposed) {
            disposable.dispose()
            Timber.e("disposable disposed")
        }

    }