어떠한 클래스(차)를 만들때 생성자에서 다른 클래스 인스턴스(엔진)를 불러와 생성을 하게 된다면, 차의 dependency는 engine이게 된다. 우리는 retrofit을 사용하기 위해 dependency를 주입하고, room을 사용하기 위해서도 주입한다. 중요한 것은 이 의존성들이 결합도를 낮게 가져가야 예기치 못한 문제 상황에서 해결하기가 수월하다는 것이다. dagger는 의존성 주입을 통해 서로 의존적인 클래스들의 외부 선언을 일일히 하지 않도록 도와준다.
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
implementation 'com.google.dagger:dagger:2.40.5'
kapt 'com.google.dagger:dagger-compiler:2.27'
https://github.com/google/dagger 최근 버전을 여기서 확인한다.
간단한 예로 스마트폰 클래스를 만들어보았다.
class SmartPhone (val battery: Battery, val simCard: SIMCard, val memoryCard: MemoryCard) {
init {
battery.getPower()
simCard.getConnection()
memoryCard.getSpaceAvailablity()
Log.i("MYTAG", "SmartPhone Constructed")
}
fun makeACallWithRecording() {
Log.i("MYTAG", "Calling.....")
}
}
스마트폰을 만들기 위해서는 배터리, 심카드, 메모리카드가 있어야 하므로 battery,simCard, memoryCard에 의존적이다. 또한 simCard역시 serviceProvider 클래스를 인자로 받아 만들 수 있기 때문에 이에 의존적이다.
class SIMCard (private val serviceProvider: ServiceProvider) {
init {
Log.i("MYTAG","SIM Card Constructed")
}
fun getConnection(){
serviceProvider.getServiceProvider()
}
}
각각의 클래스에 @Inject constructor
로 생성자를 명시해준다.
심카드 클래스의 경우,
class SIMCard @Inject constructor(private val serviceProvider: ServiceProvider) {
init {
Log.i("MYTAG","SIM Card Constructed")
}
fun getConnection(){
serviceProvider.getServiceProvider()
}
}
그리고 SmartPhoneComponent 인터페이스 클래스를 만들어 가장 상위 클래스인 SmartPhone을 부를 수 있는 클래스를 만들고 Component 애너테이션을 해준다.
@Component
interface SmartPhoneComponent {
fun getSmartPhone() : SmartPhone
}
re-build를 해주면 dagger에서 자동으로 injection에 해당하는 클래스들의 의존성을 적어주며, 이제 main을 수정해주면 된다.
smartPhone 객체를 부르는 메인 액티비티를 수정해준다.
class MainActivity : AppCompatActivity() {
private lateinit var smartPhone: SmartPhone
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
DaggerSmartPhoneComponent.create()
.getSmartPhone()
.makeACallWithRecording()
// val smartPhone = SmartPhone(
// Battery(),
// SIMCard(ServiceProvider()),
// MemoryCard()
// )
// .makeACallWithRecording()
}
}
일일히 Battery() 등의 클래스 생성을 하지 않고 getSmartPhone메서드로 SmartPhone을 만들고 이의 makeACall을 실행시키는 같은 방법으로 동작하는 함수가 완성되었다.
많은 경우에서는 우리가 클래스를 직접 수정하는 것이 불가능할 때가 있다. 예를 들어 Retrofit을 사용할 때 등이 있는데, 이때는 새로 인스턴스를 만들어주기 위한 module을 만들어 @Provides 애너테이션을 넣음으로서 대신할 수 있다.
class MemoryCard{
init {
Log.i("MYTAG","Memory Card Constructed")
}
fun getSpaceAvailablity(){
Log.i("MYTAG","Memory space available")
}
}
MemoryCard 클래스가 위처럼 inject를 하지 않는 다고 가정해보자.
MemoryCardModule
@Module
class MemoryCardModule {
@Provides
fun providesMemoryCard(): MemoryCard{
return MemoryCard()
}
}
위가 자신에게 의존적인 클래스에 자신을 전달한다는 의미의 @inject constructor 와 같은 역할을 하는 클래스이다.
interface Battery {
fun getPower()
}
기존의 배터리 클래스를 인터페이스로 바꾸고 로직을 따로 구현할 때는 Inject가 있더라도 따로 모듈을 만들어주어야 한다.
NickelCadmiumBattery.kt
class NickelCadmiumBattery @Inject constructor():Battery {
override fun getPower(){
Log.i("MYTAG" , "Power from NickelCadmiumBattery")
}
}
NCBatteryModule
@Module
class NCBatteryModule {
@Provides
fun providesNCBattery(nickelCadmiumBattery: NickelCadmiumBattery):Battery{
return nickelCadmiumBattery
}
}
심지어 어차피 Battery 형이 nickelCadmiumBattery 하나만 있으므로 추상 클래스로 만들어
@Module
abstract class NCBatteryModule {
@Binds
abstract fun bindsNCBattery(nickelCadmiumBattery: NickelCadmiumBattery):Battery
}
위처럼 만들어줘도 된다.
SmartPhoneComponent의 getter 메서드로 필요한 액티비티마다 이 메서드를 콜하는 것을 막기 위해 field injection이라고 하여 클래스 안에 의존성 주입을 해줄 수 있다.
@Component(modules = [MemoryCardModule::class,NCBatteryModule::class])
interface SmartPhoneComponent {
fun inject(mainActivity: MainActivity)
}
먼저 컴포넌트 클래스를 위처럼 수정하여 액티비티를 인자로 받는 inject 함수를 만들고,
@Inject
lateinit var smartPhone: SmartPhone
main에서 smartPhone 클래스를 inject해준다음 component의 inject함수를 불러 smartPhone의 메서드를 가져올 수 있도록 한다.
class MainActivity : AppCompatActivity() {
@Inject
lateinit var smartPhone: SmartPhone
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
DaggerSmartPhoneComponent.create()
.inject(this)
smartPhone.makeACallWithRecording()
}
@Module
class MemoryCardModule(val memorySize:Int) {
@Provides
fun providesMemoryCard():MemoryCard{
Log.i("MYTAG" , "Size of Memory is $memorySize")
return MemoryCard()
}
}
메모리카드 모듈에 위와 같이 Size라는 인자를 받는다면 기존처럼 create() 함수만으로 smartPhone을 만들 수 없다. 이럴 때에는 create() 대신 builder()
를 사용해준다.
class MainActivity : AppCompatActivity() {
@Inject
lateinit var smartPhone: SmartPhone
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
DaggerSmartPhoneComponent.builder()
.memoryCardModule(MemoryCardModule(1000))
.build()
.inject(this)
}
}
main activity에서 smartphone을 부르기 위한 build() 등등의 일련의 코드를 여러 액티비티에서 사용하고 싶다면 application class를 만들어 사용해주어야 한다.
SmartPhoneApplication.kt
class SmartPhoneApplication : Application() {
lateinit var smartPhoneComponent: SmartPhoneComponent
override fun onCreate() {
super.onCreate()
smartPhoneComponent = initDagger()
}
private fun initDagger() : SmartPhoneComponent = DaggerSmartPhoneComponent.builder()
.memoryCardModule(MemoryCardModule(1000))
.build()
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
@Inject
lateinit var smartPhone: SmartPhone
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
(application as SmartPhoneApplication).smartPhoneComponent
.inject(this)
}
}
manifest.xml
<application
android:name=".SmartPhoneApplication" ...
rotateapp과 같은 상황에서 oncreate안의 inject 함수를 통하여 smartPhone이 다시 재생성되는 불상사를 막기 위해 singleton 애너테이션으로 클래스를 구성할 수 있다
smartPhone.kt
@Singleton
class SmartPhone @Inject constructor(val battery: Battery, val simCard:
.
.
.
smartPhoneComponent
@Singleton
@Component(modules = [MemoryCardModule::class,NCBatteryModule::class])
interface SmartPhoneComponent {
fun inject(mainActivity: MainActivity)
}