티스토리 뷰

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)

해당 글은 '아키텍처를 알아야 앱 개발이 보인다' 를 공부하며 요약 정리한 글 입니다.

댓글
최근에 올라온 글
최근에 달린 댓글
네이버 이웃추가
«   2024/03   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함