LoginSignup
86
73

More than 5 years have passed since last update.

Android Dagger2 with Kotlin

Last updated at Posted at 2017-11-04

実用的なライブラリをKotlin交えて紹介しようプロジェクト2

今回はコードをより見やすくしたり、
Unitテストを簡単に行うようにしたりする
Dagger2についての基本的な使い方をあらっていこうと思います。

私自身、かなりふわっと理解している程度なので、間違っていたらご指摘お願いします。。

前回のKotlin with Databindingのプロジェクトに
ブランチを切ってDagger2をあてているので、参考にどうぞ。
https://github.com/HoNKoT/KotlinAndroidDatabindingSample/pull/1

Dagger2

Googleの提供している、JavaとAndroidのための
Dependency Injection 用フレームワークライブラリのことです

Dependency Injection

よく DI と略されます。

コンポーネント間における依存関係を
コードからなくそうぜ、という考え方のことを言います。

依存関係のあるクラス構成

依存関係?なにそれおいしいの?
ということで、例えばこんなクラスがあったとしましょう

class A {
    val mInstanceB: B

    constructor(instanceB: B) {
        mInstanceB = B
    }

    // ... 移行、mInstanceBを利用したメソッドが多数
}

このクラスAは、クラスBを利用して何かを行うことが多くなる設計かと予想されます。
この場合、クラスAはクラスBに依存していると言えます。
だってクラスBがないと、クラスAって動かないでしょ?

では例えばActivityでこれを利用する場合、
セットアップにクラスBまで作成する必要が発生しますよね?


lateinit var mInstanceA: A

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

    val instanceB = B()
    mInstanceA = A(instanceB)
}

このクラスBをインスタンス化することは本来、
Activityは見えなくてもいいわけです。

これを書くことで、不必要なインスタンスに気をまわしたり、
不要なテストコードを書く必要が発生してきます。
そのため メンテナンスコストがどんどん肥大化していく という問題があります。

Dagger利用例

ではDagger2を利用するとどうなるのでしょうか?


@Inject lateinit var mInstanceA: A

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    component.inject(this)
}

基本的にこれだけです。

この injectのタイミングで、@Injectアノテーションを指定しているフィールドに対して、インスタンスが挿入されます。

もちろん、ここでのcomponentの取得であったり、
インスタンス挿入時の定義など、やることは他にもたくさんありますが、
Activityに書くのはたったこれだけになります。

メリット

  • 可読性の向上
  • テストコードの簡易化

デメリット

ないです

強いて挙げるのであれば、Dependency Injectionってなに?
となっている人に対しては学習コストが少し高い、というくらいでしょうか。

また、アプリからするとあまり聞かないのかもしれませんが、
このDIという考え方はサーバ側の技術では割と一般的らしいので、
考え方は知っておくべきかと思います。

あとは、小さい規模のプロジェクトであれば恩恵は少ないかもしれませんね。

使い方

Gradle

まずはライブラリを入れましょう

build.gradle
apply plugin: 'kotlin-kapt'

dependencies {
    compile "com.google.dagger:dagger:2.12"
    annotationProcessor "com.google.dagger:dagger-compiler:2.12"
    kapt "com.google.dagger:dagger-compiler:2.12"
}

テストでも利用したい場合はこちらも必要です

build.gradle
dependencies {
    kaptTest "com.google.dagger:dagger-compiler:2.12"
}

Module/Provide

自動生成するインスタンス自体、あるいはその引数に必要なインスタンスの引き渡し方法を定義するクラスを @Module アノテーションを使って宣言します。
また、このModuleクラスを通じて生成するインスタンスの宣言を @Provide アノテーションで宣言します。

一応命名規則として、provide + 生成するクラス名 でメソッドを定義するというのがあります。

@Module
class AppModule(private val application: Application) {

    @Provides
    fun provideContext(): Context {
        return application
    }
}

また、これらインスタンスは @Singleton をつけることで、
簡単にシングルトンにすることができます。
(詳細は後述)

@Module
class ModelModule {

    @Provides
    @Singleton
    fun providePersonPresenter(): PersonPresenter {
        return PersonPresenter()
    }
}

この例ではPersonPresenterインスタンスがシングルトン扱いされます

Component

実際にインスタンスを挿入する対象クラスを定義するインターフェース宣言です
そのインターフェースがどのModuleを利用しているかを同時に宣言します

@Singleton
@Component(modules = arrayOf(
        AppModule::class,
        ModelModule::class)
)
interface AppComponent {
    fun inject(activity: BMIRealtimeCalculateActivity)
}

ここで inject したクラスに宣言されているフィールドにインスタンスが挿入されます。
(このケースでいうと、BMIRealtimeCalculateActivityに宣言されているインスタンス。継承先などは含みません)

Inject

では実際にインスタンスを注入してみましょう
方法は主に2通りあります
- Fieldへの注入
- Class(コンストラクタ)の引数への注入

Builder

まず作成したComponentを作成しましょう

Moduleを作成してビルドすると、Dagger + Componentクラス名でクラスが作成されているはずなので、
そのBuilderを利用して作成します。

利用するModuleをAddしていくことで、作成するComponentに含んでゆく

        val appComponent = DaggerAppComponent.builder()
                .appModule(AppModule(this))
                .modelModule(ModelModule())
                .build()

Field

可変変数の前に @Inject をつけます。
NonNullにしたいことが大抵だと思いますので、lateinitをつけておきましょう

    @Inject lateinit var personPresenter: PersonPresenter

Class

クラス自体のインスタンス化、つまりコンストラクタの引数に対して注入できます
やりかたは簡単で、プライマリコンストラクタの前に @Inject をつければ引数にたいして効果を発揮します

利点としては private val で宣言できる ことでしょう

class BMIRealtimeCalculateViewModel @Inject constructor(private val mPresenter: PersonPresenter)  

Singleton

これをつけたprovidedな要素はシングルトンになります(そのまんま)

このシングルトンな期間は、 Componentの生存期間 に依存します。
そのため、Androidで完全にSingletonとして働かせようとする場合は、
Applicationクラスに作成することが好ましいと言えます。

CustomApplication.kt
class CustomApplication : Application() {

    private lateinit var appComponent: AppComponent

    fun getComponent(): AppComponent {
        return appComponent
    }

    override fun onCreate() {
        super.onCreate()

        appComponent = DaggerAppComponent.builder()
                .appModule(AppModule(this))
                .modelModule(ModelModule())
                .build()
    }
}
AndroidManifest.xml
    <application
        android:name=".CustomApplication"
         :

そしてActivityでこのように利用する

BMIRealtimeCalculateActivity.kt
class BMIRealtimeCalculateActivity : AppCompatActivity() {

    private val mBinding: ActivityBmiRealtimeCalculateBinding by lazy {
        DataBindingUtil.setContentView<ActivityBmiRealtimeCalculateBinding>(this, R.layout.activity_bmi_realtime_calculate)
    }

    @Inject lateinit var viewModel: BMIRealtimeCalculateViewModel

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

        // Inject
        (application as? CustomApplication)?.getComponent()?.inject(this)

        mBinding.viewModel = viewModel
    }
}

では、@Singletonを利用するシーンを考えてみます

Moduleに記載する

こうすると、SessionManagerをInject指定した場合には、
何度コールしても同じSessionManagerインスタンスが挿入されます

@Module
class AppModule(private val application: Application) {

    @Singleton
    @Provides
    fun provideSessionManager(): SessionManager {
        return SessionManager()
    }
}

つまり、SessionManagerはこのアプリケーションプロセスの中でシングルトンになるわけですね。

Classに記載する

これは @Provideで定義しないクラスに対して有効です
例えばLayoutInflaterに依存するResourceProviderがあったとします

ResourceProvider.kt
class ResourceProvider @Inject constructor(private val context: Context) {
    // ...
}

そしてこのResourceProviderに依存するViewModelがあったとします

TestViewModel.kt
class TestViewModel @Inject constructor(private val resourceProvider: ResourceProvider) {
    // ...
}

このとき、TestViewModelは@Provide指定する必要はありません。
ではこのTestViewModelを仮にシングルトンにしたい場合、どうすればいいのでしょうか?

BMIRealtimeCalculateActivity.kt
class BMIRealtimeCalculateActivity : AppCompatActivity() {

    @Inject lateinit var viewModel: TestViewModel

そんなとき、TestViewModelをシングルトンにしたい場合、Classに@Singletonをつけます。

TestViewModel.kt
@Singleton
class TestViewModel @Inject constructor(private val resourceProvider: ResourceProvider) {
    // ...
}

Binds

例えばあるinterfaceを実装したクラスがあり、
操作するのはこのinterfaceを通して行いたい、ということがあると思います。

そんなときは @Binds を指定することで、
Injectの際にインターフェースの形でインスタンスを自動生成することができます

AccountApiClient.kt
interface IAccountApi {
    fun login(String id, String pass): Single<Session>
}

class AccountApiClient : IAccountApi {
    override fun login(String id, String pass): Single<Session> {
        return Single.create { subscriber ->
            // ... API通信処理
            subscriber.onSuccess(session)
        }
    }
}
SessionManager.kt
class SessionManager @Inject constructor(private val accountApi: IAccountApi) {
    // ...
}

応用

Named

Provideするインスタンスは同じでも、中身を変えたい
そんなときはProvideに名前をつけることができます

Module.kt
    @Provides
    @Named(value = "F")
    fun provideGenderFemale(): Gender {
        return Gender.Female
    }

    @Provides
    @Named(value = "M")
    fun provideGenderMale(): Gender {
        return Gender.Male
    }
Activity.kt
    // NOTE: Use this instead of @Inject @Named(value = "F")
    @field:[Inject Named("F")] lateinit var gender: Gender

Scope

Scopeとは、注入(@Provide)するインスタンスの生存期間を指定する仕組みです
実は先に説明した@SingletonはDagger2で標準サポートされているScopeです

さらにこのScopeは、ユーザが自分で作成することも可能です。
つまり ActivityやViewModel単位でのシングルトン指定も可能 になります。

Custom Scope

宣言自体は以下のようにアノテーション定義を行うだけです
JavaとKotlinでちょっと違うので並べて書いておきます

ViewModelScope.java
@Scope
@Retention(RetentionPolicy.RUNTIME)
@interface ViewModelScope {
}
ViewModelScope.kt
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ViewModelScope

注意点

注意点として、アノテーションを定義したところで、
それにそったComponentの運用を行わなければ意味がありません

例えば、ViewModel単位であればBaseViewModelのようなインスタンス内で使い回すComponentを作成する必要がありますし、
Activity単位であればBaseActivityの中で使い回すComponentを作成する必要があります。

先に挙げた例は一般的なシングルトン(Javaでいうstatic)な意味合いなので、
Applicationの中で使い回すComponentを実装しています

SubComponent

Componentの親子関係を定義することが可能です。

注意点として、親に定義したScopeが定義できないので、
Scopeを分ける必要があります。(逆に言うと、Scopeを分けるときに利用します)

例)Main Component

AppComponent.kt
@Singleton
@Component(modules = arrayOf(
        AppModule::class)
)
interface AppComponent {

    fun inject(customApplication: CustomApplication)

    fun plus(module: ActivityModule): ActivityComponent
}
AppModule.kt
@Module
class AppModule(private val mContext: Context) {

    @Provides
    fun provideContext(): Context {
        return mContext
    }

    @Provides
    fun provideLayoutInflater(): LayoutInflater {
        return LayoutInflater.from(mContext)
    }

    @Singleton
    @Provides
    fun provideSharedPreference(): SharedPreferences.Editor {
        return mContext.getSharedPreferences("Default", Context.MODE_PRIVATE).edit()
    }

//    @Singleton
//    @Provides
//    fun providePersonPresenter(): PersonPresenter {
//        return PersonPresenter()
//    }
}

例)Sub Component

ActivityComponent.kt
@ActivityScope
@Subcomponent(modules = arrayOf(ActivityModule::class))
interface ActivityComponent {

    fun inject(activity: BMIRealtimeCalculateActivity)
}
ActivityModule.kt
@Module
class ActivityModule(private val mActivity: Activity) {

    @Provides
    fun provideActivity(): Activity {
        return mActivity
    }

    @ActivityScope
    @Provides
    fun providePersonPresenter(): PersonPresenter {
        return PersonPresenter()
    }
}

例)Componet作成

CustomApplication.kt

    override fun onCreate() {
        super.onCreate()

        appComponent = DaggerAppComponent.builder()
                .appModule(AppModule(this))
                .build().also { it.inject(this) }
    }
BaseActivity.kt
    val mComponent: ActivityComponent get() = (application as CustomApplication).getComponent()
            .plus(ActivityModule(this))

Android向け

Dagger2にはAndroid向けに
ActivityやFragmentに対してのInjectorが搭載されたサブライブラリがあります。

ただこれも個人的には結構難しいイメージがあり、
それならFragment/Activityの基底クラスを作ってそれぞれのSubComponentを利用すればいいんじゃ。。と思っちゃいます。

NonExistentClass

ビルドをすると、上記エラーが発生するケースがあるかもしれません。
これはKotlinでビルドすると起きがちなものみたいですが、
以下をgradleに追加すると、幸せになれるかもしれません

build.gradle
kapt {
    correctErrorTypes = true
}

ドキュメントによると、参照できなかったものはNonExistentClassにして処理しちゃうよ、
みたいなことだと思うんですが、それを無視してそのままのクラス名使うよ、というオプション?ではないかと推測。

Some annotation processors (such as AutoFactory ) rely on precise types in declaration signatures. By default, Kapt replaces
every unknown type (including types for the generated classes) to NonExistentClass , but you can change this behavior. Add
the additional flag to the build.gradle file to enable error type inferring in stubs:

kapt {
    correctErrorTypes = true
}

Note that this option is experimental and it is disabled by default.

ぼくはOrmaと併用したときにこれが発生して泣きましたが、
これで解決しました!

Android Studio Plugin

ダイレクトにProvidesとModuleがつながっているのであれば
リンクですぐに参照が可能なプラグインが用意されているので、
必要な方は入れてみてもいいかもしれません。

inject-to-provide.gif

86
73
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
86
73