본문 바로가기

[Android][Kotlin] Dagger2 #6 - Retrofit + DaggerApplication Factory 예시

Dagger2 #6 - Retrofit 사용 예시

 

Dagger 관련 글


 

Dagger + Retrofit

  • 이전 글, [ Android Dagger 사용방법 ] 중 하나인 DaggerApplication + DaggerActivity와 Retrofit 사용예시
  • REST API -> Github API의 User 리스트 (30명)
    URL : "https://api.github.com/users"
    더보기
    [
      {
        "login": "mojombo",
        "id": 1,
        "html_url": "https://github.com/mojombo",
        ...
      },
      {
        "login": "defunkt",
        "id": 2,
        "html_url": "https://github.com/defunkt",
        ...
      },
      {
        "login": "pjhyett",
        "id": 3,
        "html_url": "https://github.com/pjhyett",
        ...
      },
      ...
      ...
      {
        "login": "bmizerany",
        "id": 46,
        "html_url": "https://github.com/bmizerany",
        ...
      }
    ]

    : Github API에서 제공해주는 30명의 User 리스트를 얻어오는 Retrofit을 Dagger로 DI(의존성 주입) 구현 예시

    User의 여러 정보를 API로 제공해주는데 그 중 Login / id / HTML_URL 3가지만 얻어오겠습니다

  • 이전 5번 글 Android Dagger 사용방법의 코드에서 추가로 작성
    Project 구조
    1. activity 패키지 : RetrofitActivity 추가 (이전 글들과 분리를 위해 Activity 추가 선언)
    2. module 패키지 : RetrofitModule 추가 (Retrofit DI) + ActivityBindingModule 수정 (RetrofitActivity 추가)
    3. model 패키지 : User 데이터 클래스 추가 (Github API 데이터를 파싱할 Data 클래스)
    4. network 패키지 : GithubAPI 인터페이스 추가 (Retrofit 함수 선언)
    5. AppComponent에 RetrofitModule 추가선언 (모듈 등록)
      * 빨간색상의 클래스/인터페이스들이 추가된 구성요소입니다

Project 구조

 

1. Gradle 설정 (Retrofit 라이브러리)

Retrofit을 사용하기 위해 가장 먼저 Gradle에 Retrofit의 Dependencies 추가

build.gradle(app)
    implementation 'com.squareup.retrofit2:retrofit:2.6.4'              // Retrofit2 라이브러리
    implementation 'com.squareup.retrofit2:converter-gson:2.6.4'        // GsonConverter 라이브러리
    implementation 'com.squareup.okhttp3:logging-interceptor:4.4.1'     // loggingInterceptor 라이브러리

Retrofit + 얻어온 데이터를 변환하기 위한 GsonConverter + Rest API 요청 로깅 확인 위한 LoggingInterceptor 추가

 

2. User 데이터 클래스 선언

REST API로 반환받는 데이터를 변환해서 저장할 Data class 선언

요청할 Github API의 URL주소는 "https://api.github.com/users"이며, 반환타입은 아래와 같습니다

Github API 반환 데이터 구조
{
  "login": "mojombo",	// 👈 저장할 속성 1
  "id": 1,		// 👈 저장할 속성 2
  "node_id": "MDQ6VXNlcjE=",
  "avatar_url": "https://avatars0.githubusercontent.com/u/1?v=4",
  "gravatar_id": "",
  "url": "https://api.github.com/users/mojombo",
  "html_url": "https://github.com/mojombo",		// 👈 저장할 속성 3
  "followers_url": "https://api.github.com/users/mojombo/followers",
  "following_url": "https://api.github.com/users/mojombo/following{/other_user}",
  "gists_url": "https://api.github.com/users/mojombo/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/mojombo/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/mojombo/subscriptions",
  "organizations_url": "https://api.github.com/users/mojombo/orgs",
  "repos_url": "https://api.github.com/users/mojombo/repos",
  "events_url": "https://api.github.com/users/mojombo/events{/privacy}",
  "received_events_url": "https://api.github.com/users/mojombo/received_events",
  "type": "User",
  "site_admin": false
}

반환 데이터의 구조는 위 처럼 많은 데이터를 포함합니다. 이 중 login / id / html_url 데이터만 저장하기 위한 User 데이터 클래스의 선언은 아래와 같습니다

User.kt
data class User (
    @SerializedName("login")
    private var login: String,
    
    @SerializedName("id")
    private var id: Int,
    
    @SerializedName("html_url")
    private var htmlUrl: String
)

User 데이터 클래스는 3개의 프로퍼티(login, id, htmlUrl)을 포함합니다

프로퍼티명을 자유롭게 사용하기 위해 API 반환데이터 속성과 @SerializedName의 이름을 일치시켜 줍니다

선언한 3개의 속성 외의 값들은 자동으로 버려지게 됩니다

 

3. GithubApi 인터페이스 선언 (Retrofit 메서드 구현)

Retrofit 구조 명세의 Interface를 선언합니다

interface GithubApi {

    @GET("users")
    fun getUsers() : Call<List<User>>
    
}

Dagger에서 Retrofit를 DI(의존성 주입)하여 사용하는 예시를 보기 위해, 간단히 User목록만 조회하는 getUsers() 메서드 하나만 선언했습니다.

@GET 요청으로 단순히 REST API에 데이터만 요청하고, 반환값은 Call<List<User>> 타입으로 반환받도록 구현합니다

 

4. RetrofitModule 선언

Retrofit 인스턴스를 관리해줄 Module을 새로 선언합니다

RetrofitModule
@Module
class RetrofitModule {

    @Provides
    @Singleton
    fun provideGithubApi(okHttpClient: OkHttpClient, factory: Converter.Factory): GithubApi {	// 1️⃣
        return Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .addConverterFactory(factory)
            .client(okHttpClient)
            .build()
            .create(GithubApi::class.java)
    }

    @Provides
    @Singleton
    fun provideOkHttpClient(interceptor: HttpLoggingInterceptor): OkHttpClient {	// 2️⃣
        return OkHttpClient.Builder().addInterceptor(interceptor).build()
    }


    @Provides
    @Singleton
    fun provideLoggingInterceptor(): HttpLoggingInterceptor {	// 3️⃣
        return HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
    }


    @Provides
    @Singleton
    fun provideConverterFactory(): Converter.Factory {	// 4️⃣
        return GsonConverterFactory.create()
    }
}

1️⃣ GithubApi를 반환합니다 (Retrofit 메서드 선언한 interface 구현체)
    : Retrofit 인스턴스를 생성하고, create()함수로 Interface 구현체를 반환합니다
     Retrofit 인스턴스 생성에 필요한 HttpClient + GsonFactory 객체는 다른 @Provides 메서드를 통해
     Component가 제공합니다 

2️⃣ OkHttpClient 반환 
    : HttpLoggingInterceptor를 등록한 OkhttpClient 인스턴스를 반환합니다.
     Component에 의해 Retrofit 의존성메서드에 제공됩니다

3️⃣ HttpLoggingInterceptor 반환
    : Restful 요청 과정 확인을 위한 Logging Interceptor 인스턴스를 제공합니다

4️⃣ Converter.Factory 반환
    : GsonConverterFactory.create()로 GsonConverter.Factory를 생성하지만, Converter.Factory로 캐스팅하여 제공합니다

RetroftModule에서 제공하는 인스턴스는 모두 @Singleton으로 싱글톤 구성을 하도록 설정했습니다.

Component에도 @Singleton을 똑같이 선언해줘야 합니다

Retrofit 인스턴스를 생성하는 Module을 선언하였으니 AppComponent에 등록을 합니다

 

5. ContextModule 선언

Component에게서 Component가 갖고있는 @BindsInstance application을 제공받아 Context 인스턴스로 캐스팅해서 반환하는 Module  

ContextModule.kt
@Module
abstract class ContextModule {

    @Binds
    abstract fun provideContext(application: Application) : Context	// 1️⃣
}

1️⃣ @Binds 메서드로 선언
    : 매개변수(application)을 Component에게 요청해서 제공받아 Context 인스턴스로 캐스팅하여 반환
     Component는 application을 @BindsInstance로 선언하여 구성요소로 갖고있다가 요청시 제공

 

6. Component에 RetrofitModule + Component.Factory 선언

AppComponent.interface
// Application 단위 클래스에 사용될 AppComponent : 최상위 Component
@Component(modules = [
        // AndroidInjectionModule::class,       // HasAndroidInjector, DispatchingAndroidInjector 직접 구현할 때 사용
        AndroidSupportInjectionModule::class,   // DaggerApplication, DaggerActivity로 HasAndroid,Dispatching 구현을 위임할 때 사용
        ActivityBindingModule::class,           // SubComponent.Factory 바인딩 목적의 Module
        CoffeeModule::class,                     // Coffee 인스턴스 바인딩을 위한 Module
        RetrofitModule::class,	// 1️⃣
        ContextModule::class	// 2️⃣
])
@Singleton			// 3️⃣
interface AppComponent : AndroidInjector<MyApp> {       // DaggerApplication 사용하려면 AndroidInjector<> 상속 필요

        fun coffees() : Map<Class<*>, Coffee>           // Coffee MultiBinding의 Provision 메서드
        
        @Component.Factory
        interface Factory {	
            fun create(@BindsInstance application: Application) : AppComponent	// 4️⃣
        }
}

1️⃣ RetrofitModule 등록
    : Retrofit 인스턴스 생성을 관리하는 모듈

2️⃣ ContextModule 등록
    : Context 인스턴스 생성을 관리하는 모듈

3️⃣ @Singleton Scope 설정
    : RetrofitModule에서 제공하는 Retrofit 인스턴스는 싱글톤으로 구현되도록 선언했기 때문에,
     연결된 Component에도 동일한 Scope를 선언해줘야 해당 객체를 제공해줄 수 있습니다

4️⃣ @BindsInstance application: Application
    : Dagger가 Application 인스턴스를 가지고 있으면서 Context 인스턴스가 필요한 곳에 주입하기 위해서
     @BindsInstance 어노테이션 선언 ( AppComponent가 구성요소로 갖고있으면서 Module이 필요할 경우 제공함)

 

Component.Factory는 AndroidInjector.Factory<MyApp>을 상속 시 예외 발생

     : DaggerApplication의 applicationInjector는 AndroidInjector<out DaggerApplication> 타입으로 create의 반환 Type이 AndroidInjector.Factory로 추상회되면 Dagger가 구분할 수 없기 때문에 정확한 반환Type을 요청합니다

 

+ ActivityBindingModule에 RetrofitActivity Component 추가 

저는 RetrofitActivity를 별도로 사용했기 때문에 ActivityBindingModule에서 추가가 필요합니다

ActivityBindingModule.kt
@Module(subcomponents = [MainComponent::class, DetailComponent::class])
abstract class ActivityBindingModule {

    @Binds
    @IntoMap
    @ClassKey(MainActivity::class)  // MultiBinding, Map 컬렉션의 Key Type
    abstract fun bindMainComponentFactory(factory: MainComponent.Factory): AndroidInjector.Factory<*>

    @Binds
    @IntoMap
    @ClassKey(DetailActivity::class)
    abstract fun bindDetailComponentFactory(factory: DetailComponent.Factory): AndroidInjector.Factory<*>

    /** SubComponent와 SubComponent.Factory 내부 메서드 X, 부모 X 경우
     *  즉, 내용이 없는 경우엔 @ContributesAndroidInjector 애노테이션으로 SubComponent 인터페이스 선언을 대체해서 사용
     *      : SubComponent 인터페이스 생성하지 않아도 SubComponent 구현을 가능하게 함
     *        modules, @Scope 설정 모두 동일하게 가능
     */
    @ContributesAndroidInjector
    abstract fun retoriftActivity() : RetrofitActivity	// 1️⃣ 추가
}

전에 선언해놨었던 MainActivity와 DetailActivity는 별도로 SubComponent 인터페이스를 선언해서 사용했지만,

RetrofitActivity는 @ContributesAndroidInjector 어노테이션을 사용해서 간단하게 추가했습니다.

위의 MainComponent와 DetailComponent도 모두 별도의 구현메서드가 없기 때문에 모두 @ContributesAndroidInjector 어노테이션으로 변경이 가능합니다

 

7. DaggerApplication에서 Component.Factory 사용방법

MyApp.kt
class MyApp : DaggerApplication() {
    /**
     * Implementations should return an [AndroidInjector] for the concrete [ ]. Typically, that injector is a [dagger.Component].
     */
    // DaggerApplication의 구현메서드 (재정의)
    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        return DaggerAppComponent.factory().create(this@MyApp) // 1️⃣
    }

    override fun onCreate() {
        super.onCreate()
    }
}

1️⃣ DaggerAppComponent.factory().create(this@MyApp)
    : 기존의 DaggerAppComponent.create()에서 수정, @BindsInstance로 선언한 application 처럼 Component 초기화가
     필요할 경우 커스텀 create()함수를 사용하는 방법

[ Component.Factory 또는 Builder + @BindsInstance 사용법 보러가기 ] 

 

8. 의존성 주입 (Activity)

이제 Dagger를 통해 Context 인스턴스와 Retrofit 인스턴스를 의존성주입받아 사용하겠습니다 

MainActivity.kt
// DaggerActivity() : dagger-android 패키지로 DaggerComponent.Inject()를 대신하도록 설정
class MainActivity : DaggerActivity() {
    val TAG = javaClass.simpleName
    ...

    @Inject
    lateinit var context: Context	// 1️⃣

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        // Button 클릭리스너 설정 - RetrofitActivity 띄우기
        retrofitButton.setOnClickListener {
            var intent = Intent(context, RetrofitActivity::class.java)	// 2️⃣
            startActivity(intent)
        }
    }
}

1️⃣ Context 인스턴스 의존성주입 요청(@Inject) - Component는 ContextModule에게 Context를 전달받아 의존성 주입

2️⃣ 의존성주입 받은 Context 인스턴스로 Intent 설정

다음으로 RetrofitActivity로 넘어가서 Retrofit 인스턴스를 제공받아 사용합니다

RetrofitActivity.kt
class RetrofitActivity : DaggerActivity() {	// 1️⃣
    val TAG = javaClass.simpleName

    @Inject
    lateinit var githubApi: GithubApi	// 2️⃣

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        githubApi.getUsers().enqueue(object : Callback<List<User>> {	// 3️⃣
            override fun onFailure(call: Call<List<User>>, t: Throwable) {	
                Log.d(TAG, "Retrofit2 onFailure - ${t}")
            }
            
            override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) {
                if(!response.isSuccessful) return

                var userList :List<User> = response.body()!!
                Log.d(TAG, "userList Count is ${userList.size}")

                for ((index, user) in userList.withIndex())
                    Log.d(TAG, "user count $index => ${user}")
            }
        })
    }
}

1️⃣ DaggerActivity() : MainAcitivity와 마찬가지로 DaggerActivity를 상속해서 Dagger Injection을 위임

2️⃣ @inject GithubApi : Retrofit 인스턴스로 생성한 GithubApi 구현체를 의존성 주입 요청 

3️⃣ 주입받은 GithubApi로 REST API 요청을 합니다
    : onFailure(실패) 와 onResponse(성공) 콜백메서드 구현이 필수적으로 필요합니다

 

 

REST API 요청 결과

User 리스트 30명