14. 안드로이드와 Dagger2
안드로이드를 위한 기본적인 접근 방식
안드로이드의 특성
- 하나의 애플리케이션 내에서 액티비티 또는 서비스 와 같은 생명 주기를 갖는 컴포넌트로 구성
- 프래그먼트는 단독 존재 불가하며 반드시 액티비티 내에 존재해야함
- 애플리케이션을 포함한 액티비티, 서비스 등 컴포넌트는 시스템에 의해서만 인스턴스화 된다.
위 특징을 통해 아래와 같은 컴포넌트 그래프를 그릴 수 있다.
- 앱은 생명주기 동안 다양한 화면(액티비티) 및 서비스가 생성과 소멸을 반복
- 하나의 액티비티 내에서는 여러 프래그먼트가 생성 소멸 반복 가능
- 앱 생명주기와 Dagger 컴포넌트의 생명 주기를 같이 하는 앱 컴포넌트 구현
- 액티비티나 서비스를 위한 컴포넌트는 앱 컴포넌트의 서브 컴포넌트로 구성한다
- 프래그먼트는 액티비티 컴포넌트의 서브 컴포넌트로 다시 지정 한다. (서브의 서브 컴포넌트가 되겠지)
[애플리케이션 컴포넌트 생성을 위한 AppComponent]
import dagger.BindsInstance
import dagger.Component
import javax.inject.Singleton
@Component(modules = [AppModule::class])
@Singleton
interface AppComponent {
fun mainActivityComponentBuilder(): MainActivityComponent.Builder
fun inject(app: App)
/*
팩토리를 통해 생성
create() 매개 변수로 애플리케이션 컴포넌트의 모듈로 AppModule,
애플리케이션 클래스인 App을 받음
*/
@Component.Factory
interface Factory {
fun create(@BindsInstance app: App, appModule: AppModule): AppComponent
}
}
import android.content.Context
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module(subcomponents = [MainActivityComponent::class]) // 액티비티를 위한 컴포넌트는 서브 컴포넌트로 구성
class AppModule {
@Provides
@Singleton
fun provideSharedPreferences(app: App) = app.getSharedPreferences(
"default",
Context.MODE_PRIVATE
)
}
import android.app.Application
class App : Application() {
private lateinit var appComponent: AppComponent
@Override
override fun onCreate() {
super.onCreate()
appComponent = DaggerAppComponent.factory()
.create(this, AppModule())
}
fun getAppComponent() = appComponent
}
이후 App
클래스를 매니페스트에 등록
<application
...
android:name=".androidComponent.App"
>
...
</application>
- 애플리케이션 인스턴스는 시스템에 의해서만 생성가능
- 애플리케이션이 생성된 후에 팩토리의
@BindsInstance
메서드를 통해 오브젝트 그래프에 바인딩 AppModule
에서는 앱 생명 주기 동안 싱글턴으로 취급할 SharedPreference 제공- 싱글턴이 아닌 매번 생성을 원하면
@Singletone
제거 할것.
import dagger.BindsInstance
import dagger.Subcomponent
@Subcomponent(modules = [MainActivityModule::class])
@ActivityScope
interface MainActivityComponent {
fun mainFragmentComponentBuilder(): MainFragmentComponent.Builder
fun inject(activity: MainActivity)
@Subcomponent.Builder
interface Builder{
fun setModule(module: MainActivityModule): Builder
/*
엑티비티 인스턴스도 시스템에 의해 생성되므로
액티비티 생명주기 콜백 내에서 서브 컴포넌트 빌드 시 바인딩할 수 있도록 @BindsInstance 사용
setter 메서드로 액티비티 인스턴스를 바인딩
*/
@BindsInstance
fun setActivity(activity: MainActivity): Builder
fun build():MainActivityComponent
}
}
import com.lilcode.hellodagger.MainActivity
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
@Module(Subcomponent = [MainFragmentComponent::class])
class MainActivityModule {
@Provides
@ActivityScope // 해당 스코프를 사용하여 액티비티 생명주기 동안 동일한 인스턴스 제공
fun provideActivityName() = MainActivity::class.simpleName ?: "Failed get Activity simpleName"
}
스코프 정의
반응형
import javax.inject.Scope
@Scope
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope
import javax.inject.Scope
@Scope
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class FragmentScope
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:layout_width="0dp"
android:id="@+id/container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_height="0dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
import android.content.SharedPreferences
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.lilcode.hellodagger.R
import javax.inject.Inject
class MainActivity: AppCompatActivity() {
@Inject
lateinit var sharedPreferences: SharedPreferences
@Inject
lateinit var activityName: String
lateinit var component: MainActivityComponent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
component = (application as App).getAppComponent() // 매니페스트에 App을 등록했었기에.
.mainActivityComponentBuilder() // 빌더를 제공 받아
.setModule(MainActivityModule()) // 모듈 바인딩
.setActivity(this) // 인스턴스 바인딩
.build()
component.inject(this) // 의존성 주입
supportFragmentManager.beginTransaction()
.replace(R.id.container, MainFragment())
.commit()
}
@JvmName("getComponent1")
fun getComponent(): MainActivityComponent{
return component
}
}
- 애플리케이션으로 부터 AppComponent 인스턴스를 가져와서 MainActivityComponent.Builder를 제공 받아 액티비티 모듈과 인스턴스를 바인딩
- MainActivityComponent를 생성 한 뒤 의존성 주입
- AppComponent 로 부터 SharedPreference를 주입 받고, 액티비티 컴포넌트에서 String 객체 주입
import dagger.BindsInstance
import dagger.Subcomponent
@FragmentScope
@Subcomponent(modules = [MainFragmentModule::class])
interface MainFragmentComponent {
fun inject(mainFragment: MainFragment)
@Subcomponent.Builder
interface Builder {
fun setModule(module: MainFragmentModule): MainFragmentComponent.Builder
@BindsInstance
fun setFragment(fragment: MainFragment): MainFragmentComponent.Builder
fun build(): MainFragmentComponent
}
}
import dagger.Module
import dagger.Provides
import kotlin.random.Random
@Module
class MainFragmentModule {
@Provides
@FragmentScope
fun provideInt() = Random.nextInt()
}
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.fragment.app.Fragment
import javax.inject.Inject
import javax.inject.Named
class MainFragment: Fragment() {
@Inject
lateinit var sharedPreferences: SharedPreferences
@Inject
lateinit var activityName: String
@set: [Inject Named("randomNumber")]
var randomNumber: Int? = null
override fun onAttach(context: Context) {
super.onAttach(context)
if (activity is MainActivity){
(activity as MainActivity).getComponent()
.mainFragmentComponentBuilder()
.setModule(MainFragmentModule())
.setFragment(this)
.build()
.inject(this)
}
Log.d("MainFragment", activityName)
Log.d("MainFragment", "randomNUmber = $randomNumber")
/*
실행2
2021-08-02 18:39:34.958 2853-2853/com.lilcode.hellodagger D/MainFragment: MainActivity
2021-08-02 18:39:34.958 2853-2853/com.lilcode.hellodagger D/MainFragment: randomNUmber = -1329834136
실행1
2021-08-02 18:40:33.910 3342-3342/com.lilcode.hellodagger D/MainFragment: MainActivity
2021-08-02 18:40:33.910 3342-3342/com.lilcode.hellodagger D/MainFragment: randomNUmber = -709593616
*/
}
}
매니패스트 참고
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lilcode.hellodagger">
<!--
android:name=".androidComponent.App" 추가
-->
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name=".androidComponent.App"
android:theme="@style/Theme.HelloDagger">
<activity
android:name=".androidComponent.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
보일러 플레이트 코드 제거
android.dagger.*
패키지 활용하기
반응형
위 에서 구현한 안드로이드를 위한 오브젝트 그래프는 아래와 같은 문제점이 있음
- 비슷한 형태로 반복되는 보일러 플레이트 코드 생성
- 리팩토링의 어려움
- 멤버 주입 메서드의 매개변수로 정확한 타입을 알아야함
이를 위해 Dagger는 dagger.android
패키지를 제공한다.
액티비티에 의존성을 주입한다고 가정. 다시 기존 코드를 수정해보자
import dagger.Component
import dagger.android.AndroidInjectionModule
import dagger.android.AndroidInjector
import javax.inject.Singleton
/*
안드로이드 프레임워크 관련 클래스에 의존성 주입을 위임할 AndroidInjector<?> 팩토리를 멀티 바인딩으로 관리
*/
@Component(modules = [AndroidInjectionModule::class, AppModule::class])
@Singleton
interface AppComponent: AndroidInjector<App> {
@Component.Factory
interface Factory : AndroidInjector.Factory<App>{
// App 인스턴스를 그래프에 바인딩 하고 Component 를 반환하는 create() 메서드가 이미 포함
}
}
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.android.AndroidInjector
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Named
import javax.inject.Singleton
// MainActivitySubcomponent: MainActivity의 인스턴스에 멤버 인젝션을 담당
@Module(subcomponents = [MainActivitySubcomponent::class])
abstract class AppModule {
companion object{
@Named("app")
@Provides
@Singleton
fun provideString() = "String from AppModule" // 의존성 주입하는지 확인용
}
@Binds
@IntoMap
@ClassKey(MainActivity::class)
abstract fun bindAndroidInjectorFactory(factory: MainActivitySubcomponent.Factory): AndroidInjector.Factory<*>
// AndroidInjectionModule 내부에 있는 Map에 AndroidInjector.Factory를 멀티 바인딩 한다
// 서브 컴포넌트들이 편하게 멤버 인젝션을 할 수있게 인젝터 팩토리들을 멀티 바인딩으로 관리
}
import android.app.Application
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject
class App : Application(), HasAndroidInjector{
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
override fun onCreate() {
super.onCreate()
DaggerAppComponent.factory()
.create(this)
.inject(this)
}
override fun androidInjector(): AndroidInjector<Any> {
return dispatchingAndroidInjector
}
}
import dagger.Subcomponent
import dagger.android.AndroidInjector
@ActivityScope
@Subcomponent(modules = [MainActivityModule::class])
interface MainActivitySubcomponent: AndroidInjector<MainActivity> {
@Subcomponent.Factory
interface Factory: AndroidInjector.Factory<MainActivity>{
}
}
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.android.AndroidInjector
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Named
@Module(subcomponents = [MainFragmentSubcomponent::class]) // MainFragment 멤버 인젝션을 위해 서브 연결
abstract class MainActivityModule {
companion object{
@Named("activity")
@Provides
@ActivityScope
fun provideString() = "String from MainActivityModule"
}
@Binds
@IntoMap
@ClassKey(MainFragment::class)
abstract fun bindInjectorFactory(factory: MainFragmentSubcomponent.Factory): AndroidInjector.Factory<*>
// MainFragment 를 위한 인젝터 펙토리
}
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.lilcode.hellodagger.R
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject
import javax.inject.Named
class MainActivity: AppCompatActivity(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
@Inject
@Named("app")
lateinit var appString: String
@Inject
@Named("activity")
lateinit var activityString: String
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this) // 호출 시 App 으로 부터 DispatchingAndroidInjector<Any> 를 얻고
// 이를 통해 MainActivity에 맞는 AndroidInjector.Factory 클래스 이름을 통해 찾는다
// 팩토리를 통해서 생성된 MainActivitySubComponent는 액티비티에서 호출한 inject()를 통해 의존성 주입 완료
Log.e("MainActivity", appString)
Log.e("MainActivity", activityString)
/* 결과
2021-08-03 11:46:35.979 28214-28214/com.lilcode.hellodagger E/MainActivity: String from AppModule
2021-08-03 11:46:35.979 28214-28214/com.lilcode.hellodagger E/MainActivity: String from MainActivityModule
*/
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction()
.replace(R.id.container, MainFragment())
.commit()
}
override fun androidInjector(): AndroidInjector<Any> {
return androidInjector
}
}
프래그먼트도 액티비티와 동일한 방식으로 주입
import dagger.Subcomponent
import dagger.android.AndroidInjector
@FragmentScope
@Subcomponent(modules = [MainFragmentModule::class])
interface MainFragmentSubcomponent : AndroidInjector<MainFragment>{
@Subcomponent.Factory
interface Factory : AndroidInjector.Factory<MainFragment>{
}
}
import dagger.Module
import dagger.Provides
import javax.inject.Named
@Module
class MainFragmentModule {
@Named("fragment")
@Provides
@FragmentScope
fun provideString() = "String from fragment"
}
import android.content.Context
import android.util.Log
import androidx.fragment.app.Fragment
import dagger.android.support.AndroidSupportInjection
import javax.inject.Inject
import javax.inject.Named
class MainFragment: Fragment() {
@Inject
@Named("app")
lateinit var appString: String
@Inject
@Named("activity")
lateinit var activityString: String
@Inject
@Named("fragment")
lateinit var fragmentString: String
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
Log.e("MainFragment", appString)
Log.e("MainFragment", activityString)
Log.e("MainFragment", fragmentString)
/* 결과
2021-08-03 11:46:36.146 28214-28214/com.lilcode.hellodagger E/MainFragment: String from AppModule
2021-08-03 11:46:36.146 28214-28214/com.lilcode.hellodagger E/MainFragment: String from MainActivityModule
2021-08-03 11:46:36.146 28214-28214/com.lilcode.hellodagger E/MainFragment: String from fragment
*/
super.onAttach(context)
}
}
@ContributesAndroidInjector
어노테이션 활용하기
반응형
- 서브 컴포넌트의 팩토리가 다른 메서드나 클래스를 상속하지 않으면
@ContributesAndroidInjector
활용- 서브 컴포넌트 정의 코드 대체
- 보일러 플레이트 코드 줄일 수 있음
[AppComponent.kt]
import dagger.Component
import dagger.android.AndroidInjectionModule
import dagger.android.AndroidInjector
import javax.inject.Singleton
@Singleton
@Component(modules = [AndroidInjectionModule::class, AppModule::class])
interface AppComponent : AndroidInjector<App> {
@Component.Factory
interface Factory : AndroidInjector.Factory<App> {
}
}
import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector
import javax.inject.Named
import javax.inject.Singleton
@Module
abstract class AppModule {
companion object {
@Named("app")
@Provides
@Singleton
fun provideString() = "String from AppModule"
}
@ActivityScope
@ContributesAndroidInjector(modules = [MainActivityModule::class])
abstract fun mainActivity(): MainActivity
}
class App : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.factory()
.create(this)
}
}
import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector
import javax.inject.Named
@Module
abstract class MainActivityModule {
companion object {
@Named("activity")
@Provides
@ActivityScope
fun provideString() = "String from MainActivityModule"
}
@FragmentScope
@ContributesAndroidInjector(modules = [MainFragmentModule::class])
abstract fun mainFragment(): MainFragment
}
import android.os.Bundle
import android.util.Log
import com.lilcode.hellodagger.R
import dagger.android.AndroidInjection
import dagger.android.support.DaggerAppCompatActivity
import javax.inject.Inject
import javax.inject.Named
class MainActivity : DaggerAppCompatActivity() {
@Inject
@Named("app")
lateinit var appString: String
@Inject
@Named("activity")
lateinit var activityString: String
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
Log.e("MainActivity", appString)
Log.e("MainActivity", activityString)
/*
2021-08-03 21:24:44.733 8407-8407/com.lilcode.hellodagger E/MainActivity: String from AppModule
2021-08-03 21:24:44.733 8407-8407/com.lilcode.hellodagger E/MainActivity: String from MainActivityModule
*/
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction()
.replace(R.id.container, MainFragment())
.commit()
}
}
import dagger.Module
import dagger.Provides
import javax.inject.Named
@Module
class MainFragmentModule {
@Named("fragment")
@Provides
@FragmentScope
fun provideString() = "String from fragment"
}
import android.content.Context
import android.util.Log
import dagger.android.support.AndroidSupportInjection
import dagger.android.support.DaggerFragment
import javax.inject.Inject
import javax.inject.Named
class MainFragment : DaggerFragment() {
@Inject
@Named("app")
lateinit var appString: String
@Inject
@Named("activity")
lateinit var activityString: String
@Inject
@Named("fragment")
lateinit var fragmentString: String
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
Log.e("MainFragment", appString)
Log.e("MainFragment", activityString)
Log.e("MainFragment", fragmentString)
/*
2021-08-03 21:24:44.912 8407-8407/com.lilcode.hellodagger E/MainFragment: String from AppModule
2021-08-03 21:24:44.912 8407-8407/com.lilcode.hellodagger E/MainFragment: String from MainActivityModule
2021-08-03 21:24:44.912 8407-8407/com.lilcode.hellodagger E/MainFragment: String from fragment
*/
super.onAttach(context)
}
}
실행시 log가 잘 뜨는 것을 확인할 수 있다.
- Application 대신 DaggerApplicataion
- AppCompatActivity 대신 DaggerAppCompatActivity
- Fragement 대신 DaggerFragment
- 베이스 클래스 참조가 불가하다면 베이스 클래스 내부 참조하여 HasAndroidInjector 인터페이스 직접 구현
Dagger 베이스 클래스
- DispatchingAndroidInjector는 AndroidInjector.Factory를 런타임에 찾게 HasAndroidInjector를 구현 하게 됨
- 매번 AndroidInjection.inject() 호출 == 보일러 플레이트 코드
- 이를 구현할 Base 클래스를 작성할 수 있음
android.dagger
패키지 에서 제공
import dagger.android.AndroidInjector
import dagger.android.DaggerApplication
class App : DaggerApplication() {
// 애플리케이션 컴포넌트를 반환. 이로써 기존 코드 대체 가능
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.factory().create(this)
}
}
import android.content.SharedPreferences
import android.os.Bundle
import com.lilcode.hellodagger.R
import dagger.android.support.DaggerAppCompatActivity
import javax.inject.Inject
class MainActivity : DaggerAppCompatActivity() {
@Inject
lateinit var sharedPreferences: SharedPreferences
@Inject
lateinit var activityName: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction()
.replace(R.id.container, MainFragment())
.commit()
}
}
Dagger가 지원하는 기본 프레임워크 타입
- DaggerApplication
- DaggerActivity
- DaggerFragment
- DaggerService
- DaggerIntentService
- DaggerBroadcastReceiver
- DaggerContentProvider
DaggerBroadcastReceiver 사용 시 매니페스트에 브로드캐스트 리시버가 등록되어 있어야 함
직접 리시버 인스턴스 생성하는 경우라면 생성자 주입을 사용해야함
2021.08.03 - [Android/클린 아키텍처] - [Clean Architecture] 14-Dagger 서브 컴포넌트, 상속
2021.07.27 - [Android/클린 아키텍처] - [Clean Architecture] 13-Dagger 멀티 바인딩 하기
2021.07.26 - [Android/클린 아키텍처] - [Clean Architecture] 12-바인딩의 종류 (Dagger)
해당 글은 '아키텍처를 알아야 앱 개발이 보인다' 를 공부하며 요약 정리한 글 입니다.
'Android > 클린 아키텍처' 카테고리의 다른 글
[Clean Architecture] 14-Dagger 서브 컴포넌트, 상속 (0) | 2021.08.03 |
---|---|
[Clean Architecture] 13-Dagger 멀티 바인딩 하기 (0) | 2021.07.27 |
[Clean Architecture] 12-바인딩의 종류 (Dagger) (0) | 2021.07.26 |
[Clean Architecture] 11-범위 지정하기 (@Scope) (0) | 2021.07.25 |
[Clean Architecture] 10-한정자 (@named) (0) | 2021.07.24 |