84
69

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Kotlinで DI (Dependency Injection)~ Dagger 編

Last updated at Posted at 2018-06-25

AndroidアプリにDagger2を使ってDIしてみます。
DIがどんなものかというのはこちら

Daggerの他にもDIフレームワークがあります。
Koinを使ったDIはこちら

やりたいこと

ViewModelに依存するActivityがあって...

+-----------+
| Activity  |
+-----+-----+
      |
      ▽
+-----------+
| ViewModel |
+-----------+

ViewModelからデータを貰うようなアプリを作ります。

+-----------+    +-----------+
| Activity  |    | ViewModel |
+-----+-----+    +-----+-----+
      |                |
      |     greet()    |
      +--------------->|
      |<---------------+
      |      Hello     |

ViewModelActivityに注入するようにします。

プロジェクトへDagger2を導入

まずはこれがないと始まりませんので。

app/build.gradle
apply plugin: 'kotlin-kapt'

// (省略)

dependencies {

    // (省略)

    implementation      "com.google.dagger:dagger:2.14.1"
    annotationProcessor "com.google.dagger:dagger-compiler:2.14.1"
    kapt                "com.google.dagger:dagger-compiler:2.14.1"
    kaptTest            "com.google.dagger:dagger-compiler:2.14.1"
}

実装

ViewModel

class MainViewModel {
    fun greet(): String {
        return "こんにちわ"
    }
}

モジュール

DaggerのDIパターンに必要らしいです。
ViewModel のインスタンスを吐き出すマシーンのようなものです(私の解釈)。

@Module
class ViewModelModule {
    @Provides
    fun provideMainViewModel(): MainViewModel {
        return MainViewModel()
    }
}

コンポーネント

これもDaggerのDIパターンに必要らしいです。
モジュールの吐き出すインスタンスを何処に注入するのか指定するものです(私の解釈)。

@Component(modules = [ViewModelModule::class])
interface MainActivityComponent {
    fun inject(activity: MainActivity)
}

私の感覚だと「逆じゃね?」と思います。
注入される側が、どのインスタンスを欲しているのか指定する方が自然では?
まぁ、同じことですが...

いったんビルド

モジュールとコンポーネントを書いたら、いったんプロジェクトをビルドします。
そうしておかないと、このあとアクティビティで使う予定のクラスが見つからなくて困ります。
アノテーションプロセシングとやらでコードを自動生成しているようです。

アクティビティ

ViewModelを代入するプロパティをlateinitで用意しておいて、@Injectを付けておきます。

メンバーにcomponentというプロパティを定義して、そこにコンポーネントを持っておきます。
また、viewModelModuleメソッドの引数にViewModelModuleのインスタンスを指定して、コンポーネントにモジュールをロードしておきます。

injectメソッドが呼ばれると、@Injectの付いたプロパティにインスタンスが注入されます。
Activityにはコンストラクタインジェクションができないのでこうするようです。

class MainActivity : AppCompatActivity() {

    @Inject lateinit var viewModel: MainViewModel

    private val component = DaggerMainActivityComponent.builder()
            .viewModelModule(ViewModelModule())  // <-- これ
            .build()

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

        this.component.inject(this)  // <-- これ

        println("_/_/ ${this.viewModel.greet()}")
    }
}

ここまでできたら、アプリケーションを実行してみます。

結果

こんな感じでログが表示されました。
ちゃんとDIできてるようです。

logcat
06-25 00:59:09.107 3575-3575/com.example.mydaggerapp I/System.out: _/_/ こんにちわ

依存関係を追加するとどうなるか

ViewModelRepositoryに依存するようにして、その RepositoryViewModelに注入してみようと思います。

こんな感じです。

+-----------+    +-----------+    +-------------+
| Activity  |    | ViewModel |    | Repository  |
+-----+-----+    +-----+-----+    +------+------+
      |                |                 |
      |     greet()    |                 |
      +--------------->|     greet()     |
      |                +---------------->|
      |                |<----------------+
      |<---------------+     Hello       |
      |     Hello      |                 |

Repository

class GreetingRepository {
    fun greet(): String {
        return "こんにちわ from Repository"
    }
}

ViewModel

引数にGreetingRepositoryを受け取るコンストラクタを定義して@Injectを付けます。

class MainViewModel @Inject constructor(private val greetingRepository: GreetingRepository) {
    fun greet(): String {
        return this.greetingRepository.greet()
    }
}

モジュール

新たにGreetingRepositoryクラスを作ったので、モジュールも追加します。

@Module
class RepositoryModule {
    @Provides
    fun provideGreetingRepository(): GreetingRepository {
        return GreetingRepository()
    }
}

また、ViewModelModuleクラスのprovideMainViewModelメソッドの引数にGreetingRepositoryを取るようにします。
こうしておくとDaggerによってインスタンスが自動的に注入されます。

@Module
class ViewModelModule {
    @Provides
    fun provideMainViewModel(greetingRepository: GreetingRepository): MainViewModel {
        return MainViewModel(greetingRepository)
    }
}

コンポーネント

modulesRepositoryModuleクラスを追加します。

@Component(modules = [ViewModelModule::class, RepositoryModule::class])
interface MainActivityComponent {
    fun inject(activity: MainActivity)
}

ここまで書いたら例によって一旦ビルドします。

アクティビティ

RepositoryModuleのインスタンスをロードするように追記します。

class MainActivity : AppCompatActivity() {

    @Inject lateinit var viewModel: MainViewModel

    private val component = DaggerMainActivityComponent.builder()
            .repositoryModule(RepositoryModule())  // <--これ
            .viewModelModule(ViewModelModule())
            .build()

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

        this.component.inject(this)

        println("_/_/ ${this.viewModel.greet()}")
    }
}

結果

こんな感じでログが表示されました。
Repositoryからの値がちゃんとActivityまで渡ってきています。

logcat
06-25 01:52:25.556 5928-5928/com.example.mydaggerapp I/System.out: _/_/ こんにちわ from Repository

もっと実践的に

「モックを注入できるように」とか「Activityごとにいちいちコンポーネント作りたくない」とかあるのと思うので改良してみます。

Repository

インタフェースを定義して、ユニットテストとかでモックへの差し替えを容易にします。

interface GreetingRepositoryContract {
    fun greet(): String
}

class GreetingRepository : GreetingRepositoryContract {
    override fun greet(): String {
        return "こんにちわ from Repository"
    }
}

ViewModel

注入されるRepositoryの型もインタフェースにします。

interface MainViewModelContract {
    fun greet(): String
}

class MainViewModel @Inject constructor(private val greetingRepository: GreetingRepositoryContract) : MainViewModelContract {
    override fun greet(): String {
        return this.greetingRepository.greet()
    }
}

モジュール

これもRepositoryの型をインタフェースにします。

@Module
class RepositoryModule {
    @Provides
    fun provideGreetingRepository(): GreetingRepositoryContract {
        return GreetingRepository()
    }
}

@Module
class ViewModelModule {
    @Provides
    fun provideMainViewModel(greetingRepository: GreetingRepositoryContract): MainViewModel {
        return MainViewModel(greetingRepository)
    }
}

コンポーネント

アプリ全体で一つのコンポーネントを使うようにしてみます。
前項までで使っていたMainActivityComponentは不要なので削除します。

@Component(modules = [ViewModelModule::class, RepositoryModule::class])
interface AppComponent {
    fun inject(activity: MainActivity)
}

アプリケーションクラス

どこからでも参照できるように、コンポーネントをcompanion objectとして定義します。

class Application : android.app.Application() {

    companion object {
        lateinit var component: AppComponent private set
    }
}

ここまで書けたら、また例によって一旦ビルドします。

アプリケーションクラス

今までViewModelに書いてたやつをApplicationのonCreateに書きます。
これでどこからでもコンポーネントを参照できます。

class Application : android.app.Application() {

    companion object {
        lateinit var component: AppComponent private set
    }

    override fun onCreate() {
        super.onCreate()

        // ↓↓これ
        component = DaggerAppComponent.builder()
                .viewModelModule(ViewModelModule())
                .repositoryModule(RepositoryModule())
                .build()
        // ↑↑これ
    }
}

マニフェスト修正

アプリ起動時のクラスがApplicationになるようにandroid:name属性を追加します。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mydaggerapp">

    <application
        android:name=".Application"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

アクティビティ

アプリケーションクラスのcomponentからinjectを呼び出してViewModelを注入します。

class MainActivity : AppCompatActivity() {

    @Inject lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        Application.component.inject(this)  // <-- これ

        println("_/_/ ${this.viewModel.greet()}")
    }
}

結果

変わり映えしませんが、こんな感じでログが表示されました。
意図した通りに動いているようです。

logcat
06-25 02:36:40.248 7143-7143/com.example.mydaggerapp I/System.out: _/_/ こんにちわ from Repository

おわりに

と、まぁ、ひととおり試してみましたが...
ちょっと難解な感じがします。
特にコンポーネント。

つーか使いやすいか?これ??

ブログとかQiitaで紹介してくれている人たちの解説も分かり難い例ばかりで、
なぜもっと簡単に説明してくれないのだろうか?

If you can’t explain something in simple terms, you don’t understand it
"簡単な言葉で説明できないならあなたは理解していない"

リチャード・P・ファインマン

とりあえずは以上です。

84
69
2

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
84
69

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?