[Kotlin/Android] Hilt를 사용한 종속 항목 삽입

Programming/Android 2021. 9. 28. 18:36 Posted by 생각하는로뎅
반응형

1. 소개

 

  종속 항목 삽입(DI)은 프로그래밍에 널리 사용되는 기법으로, Android 개발에 적합합니다. DI의 원칙을 따르면 훌륭한 앱 아키텍처를 위한 토대를 마련할 수 있습니다.

종속 항목 삽입을 구현하면 다음과 같은 이점을 누릴 수 있습니다.

 - 코드 재사용 가능
 - 리팩터링 편의성
 - 테스트 편의성

 

 

Android의 종속 항목 삽입  |  Android 개발자  |  Android Developers

Android의 종속 항목 삽입 종속 항목 삽입(DI)은 프로그래밍에 널리 사용되는 기법으로, Android 개발에 적합합니다. DI의 원칙을 따르면 훌륭한 앱 아키텍처를 위한 토대를 마련할 수 있습니다. 종속

developer.android.com

 

p.s Spring을 해보았다면, 이해하기 훨씬 편하다.

 

 

 

2. 기본 셋팅

 

 

  1) build.gradle(Project)

 

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
    }
}

 

  2) build.gradle(app)

 

plugins {
    ...
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

android {
    ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    ...
}

dependencies {

    ...
    
    // Hilt
    implementation "com.google.dagger:hilt-android:2.38.1"
    kapt "com.google.dagger:hilt-android-compiler:2.38.1"
}

 

  3) Application Class 생성 후, @HiltAndroidApp 주석 추가(Hilt를 설정)

 

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class Application : Application() {
  ...
}

 

  4) Application Class를 AndroidManifest.xml 에 추가

 

...
    <application
        android:name=".Application"
        ...">
        ...
    </application>

...

 

  5) MainActivity 에 @AndroidEntryPoint 주석 추가 (종속항목 삽입)

 

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

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

 

 

 

3. Hilt 변수 결합하기

 

 

  1) BobModel Class 를 생성한다. 중요한 부분은 생성자 옆에 @Inject 주석을 추가해서, Hilt 에게 알려야한다. Hilt가 알고 있는 정보를 결합이라고 한다.

 

import javax.inject.Inject

class BobModel @Inject constructor(){

    var mesage : String = ""

}

 

 

  2) MainActivity Class에 변수를 선언한다.

 

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject lateinit var bobModel: BobModel

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

        // 이렇게 생성할 필요가 없다.
        // bobModel = BobModel()
        
        // Hilt가 알고 있기 때문에, 바로 사용 가능하다.
        bobModel.mesage = "bob!!"

        Log.d("sjlim/test", "${bobModel.mesage}")
    }
}

 

 

  3) 출력

 

D/sjlim/test: bob say : bob!!

 

 

 

4. 인스턴스 만들기 위한 @Singleto

 

  애플리케이션 컨테이너에서 항상 같은 인스턴스를 제공한다.

  getInstance() 패턴 형식이라고 이해하면 된다.

 

  1) BobModel Class에 @Singleton 주석을 추가한다.

 

import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class BobModel @Inject constructor(){

    var mesage : String = ""
    
}

 

 

  2) MainActivity Class를 수정한다.

 

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject lateinit var bobModel1: BobModel
    @Inject lateinit var bobModel2: BobModel

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

        bobModel1.mesage = "bob!!"

        Log.d("sjlim/test", "bob1 say : ${bobModel1.mesage}")
        
        // BobModel1 과 2는 같은 인스턴스를 참조하고 있으므로, 같은 메세지가 출력된다.
        Log.d("sjlim/test", "bob2 say : ${bobModel2.mesage}")
    }
}

 

 

  3) 출력

 

D/sjlim/test: bob1 say : bob!!
D/sjlim/test: bob2 say : bob!!

 

 

 

5. interface를 사용하기 위한 @Module과 @InstallIn

 

  추가적으로 @ActivityScoped @Binds 를 사용한다.

 

 

  1) interface 를 생성한다.

 

interface BobStateListener {
    fun happy(msg: String)
}

 

 

  2)  Interface 를 구현한 Impl 을 생성한다.

 

       Hilt 가 알 수 있도록 생성자 옆에 @InstallIn 주석을 추가해야한다.

 

import android.app.Activity
import android.util.Log
import javax.inject.Inject

class BobStateListenerImpl @Inject constructor(private val activity: Activity) : BobStateListener{

    override fun happy(msg: String) {
        Log.d("sjlim/test", "$msg")
    }

}

 

 

  2-1) 위 소스에서 activity는 아래 소스에 있는 Activity 범위를 설정했기 때문에, activity 변수는 null이 아니라, 존재하는 값으로 들어온다.

 

 

  3) interface를 이용하기 위해서는 추상화 클래스를 이용해야한다. 추상화 클래스를 생성 후, @Module과 @InstallIn을 추가한다.

 

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped

@InstallIn(ActivityComponent::class)
@Module
abstract class BobModule {

    @ActivityScoped
    @Binds
    abstract fun bindBob(impl: BobStateListenerImpl) : BobStateListener

}

 

 

  3-1) @InstallIn ( Class ??? ) 안에 Class는 왜 입력했을까?

 

  Installn 에 Class는 구성요소 범위와 표에 맞게 작성해주면 된다.

  위 소스 기준으로 본다면, BobStateListener을 Activity에서 범위에서 사용한다고 생각했고,  @ActivityScoped 주석을 입력했다. @ActivityScoped 을 사용하려면 Class 를 @InstallIn 에 입력해줘야하는데, 아래 표를 본다면 ActivityComponent와 맵핑이 되어 있어서, ActivityComponent::class를 입력해주었다.

 

Application ApplicationComponent @Singleton
View Model ActivityRetainedComponent @ActivityRetainedScope
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
@WithFragmentBindings 주석이 지정된 View ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped

 

구성요소 계층 구조

 

  3-2) @Binds 는 왜 입력했을까?

 

  인터페이스에 사용할 구현을 Hilt에 알리려면 Hilt 모듈 내 함수에 @Binds 주석을 사용하면 됩니다.

 

 

  4) interface에서 사용할 impl을 구현한다.

 

import android.app.Activity
import android.util.Log
import javax.inject.Inject

class BobStateListenerImpl @Inject constructor(private val activity: Activity) : BobStateListener{

    override fun happy(msg: String) {
        Log.d("sjlim/test", "$msg")
    }

}

 

 

  5) 이제 MainActivity에서 아래와 같이 사용 가능하다.

 

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject lateinit var bobStateListener: BobStateListener

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

        bobStateListener.happy("happy!!!")

    }
}

 

 

  6) 출력

 

D/sjlim/test: happy!!!

 

 

 

6. Module 안에 여러개의 추상화 class를 사용하려면?

 

 

  1) BobModule.kt 를 아래와 같이 수정한다.

 

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.components.FragmentComponent
import dagger.hilt.android.scopes.ActivityScoped
import javax.inject.Qualifier

@Qualifier
annotation class activityModule

@Qualifier
annotation class fragmentModule

@InstallIn(ActivityComponent::class)
@Module
abstract class BobActivityModule {

    @activityModule
    @ActivityScoped
    @Binds
    abstract fun bindBob(impl: BobStateListenerImpl) : BobStateListener

}

@InstallIn(FragmentComponent::class)
@Module
abstract class BobFragmentModule {

    @fragmentModule
    @ActivityScoped
    @Binds
    abstract fun bindBob(impl: BobStateListenerImpl) : BobStateListener

}

 

 

  2) 하나의 BobModule.kt 에 두개의 추상화 클래스를 만들었다.

     BobActivityModule 은 Activity 범위로 만들었고, BobFragmentModule는 Fragment 범위로 만들었다.

 

  

  3) @Qualifier 라인은 무엇인가?

 

     위 소스를 보면, 각 bindBob 메소드에서 BobStateListener 을 반환하고 있는것을 알 수 있다.

     Hilt 는 어느것을 사용해야할지 정확하게 판단하도록 명시를 해주기 위해서 네이밍 같은것을 해주는 것이다.

 

@Qualifier
annotation class activityModule

@Qualifier
annotation class fragmentModule

 

    이렇게 선언한다면, @activityModule 주석을 사용할 수 있다. 그래서 소스 하단 부분에 BobActivityModule 에는 

 

...
abstract class BobActivityModule {

    @activityModule
    ...
    abstract fun bindBob(impl: BobStateListenerImpl) : BobStateListener

 

  이렇게 선언해줌으로써, 명확하게 Hilt가 알 수 있도록 한다.

 

 

  4) @activityModule @fragmentModule 주석을 생성했다면, MainActivity에서 아래와 같이 사용할 변수 위에 사용이 가능하다.

 

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @activityModule
    @Inject lateinit var bobStateListener: BobStateListener

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

        bobStateListener.happy("happy!!!")

    }
}

 

 

  5) 만약 @fragmentModule 을 사용하면 어떻게 될까?

 

     위 소스는 Activity 범위를 가지고 있기 때문에, Fragment 범위는 사용할 수 없기 때문에 오류가 발생한다.

 

 

 

7. Context 를 엑세스 하려면 어떻게 해야할까?

 

  1) BobStateListenerImpl 파일에 @ApplicationContext 주석을 이용해서 사용할 수 있다.

 

import android.app.Activity
import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

class BobStateListenerImpl @Inject constructor(private val activity: Activity, @ApplicationContext private val context: Context) : BobStateListener{

    override fun happy(msg: String) {
        Log.d("sjlim/test", "$msg")
    }

}

 

 

 

 

 

 

반응형