TRY ANDROID DEV

Android アプリ開発のコーディングネタ。あとエンジニアとしての活動

DroidKaigi 2019 official appを読んでみる③:Dagger2 Android Support

背景

  • DroidKaigi 2019 のofficial appが公開されている:参照
  • せっかくだしコントリビュートしたいなと思ったけど思った以上に皆新しい技術使っててよくわからない。。
  • とりあえず一つずつ理解することにする。

コンテンツの実装を見ようとした。

Navigationを理解したので、早速各処理をみていこうと思ったのですが、

    @Inject lateinit var userActionCreator: UserActionCreator
    @Inject lateinit var systemStore: SystemStore
    @Inject lateinit var userStore: UserStore

上記StoreクラスとActionCreatorクラスがどこでインジェクトされているかわからない。。 Activity なら以下のような記述があると思ってたのに。。

    ((SampleApplication) getContext().getApplicationContext())
        .getApplicationComponent()
        .activity(this)
        .build()
        .inject(this);

どうやらDagger2のAndroid Supportを利用することで、今までOSのライフサイクルによって生成されておりInjectできなかったActivityやFragmentもInjectできるようになったそうです。まだMainActivityから出られない。。

build.gradleを編集

dagger-android-support 2.20を追加します。 なお、kotlin-kaptのplugin入れるの忘れないこと。

    implementation 'com.google.dagger:dagger:2.20'
    implementation 'com.google.dagger:dagger-android:2.20'
    implementation 'com.google.dagger:dagger-android-support:2.20'

実装してみる

今回の目標は、サンプルCoffeeMakerをActivityにInjectとすることです。

CoffeeMaker

class CoffeeMaker @Inject constructor(private val heater: Heater) {

    fun getTemperature() : String {
        return heater.getTemperature()
    }
}

Heater

class Heater @Inject constructor() {

    fun getTemperature() : String {
        return "38度"
    }
}

MainActivity

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var coffeeMaker: CoffeeMaker < ここにInjectしたい。

    private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
        when (item.itemId) {
            R.id.navigation_home -> {
                message.setText(R.string.title_home)
                return@OnNavigationItemSelectedListener true
            }
            R.id.navigation_dashboard -> {
                message.setText(R.string.title_dashboard)
                return@OnNavigationItemSelectedListener true
            }
            R.id.navigation_notifications -> {
                message.setText(R.string.title_notifications)
                return@OnNavigationItemSelectedListener true
            }
        }
        false
    }

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

        navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
    }
}

Android Type(Activity, Fragmentなど)のメンバーにインジェクションする仕組み

正直ソースコードをみててもあんまりわからなかった。。。(途中で気力が折れた)
なんとなくですが、以下の流れみたいです。

  1. ApplicationクラスにDispatchingAndroidInjectorを持たせておく
  2. Activity 起動後、ApplicationクラスがDispachingAndroidInjectorに対象のActivityを渡す
  3. するとDispachingAndroidInjectorが、うまいことActivityのメンバーにInjectしてくれるAndroidInjector.Factoryを探してくれる。
  4. このAndroidInjector.FactoryがAndroidInjectを生成できる。
  5. AndroidInjectにActivityを渡すとメンバーにInjectできる

なのでまずはAndroidInjector.Factoryを作成するところからですね。

ActivityModuleを作成する

AndroidInjection.Factoryを生成するには、二つ方法があるみたいです。 ただ特にパラメータを設定したいとかでないのであれば、MainActivityを返却するメソッドに@ContributesAndroidInjectorをつけてあげるだけでよいみたいです。

@Module
abstract class ActivityModule {

    @ContributesAndroidInjector
    abstract fun contributeMainActivityInjector(): MainActivity
}

ApplicationComponentを作成する

ApplicationクラスにDispatchingAndroidInjectorを供給するため、ApplicationComponentを作成します。 ActivityModule::classは先ほど作成したモジュールですが、 AndroidInjcectionModule::classはDaggerが用意している「Dagger フレームワークの使いやすさを確保するためのBindingを含んでいる」moduleだそうです。
なんのこっちゃですが、必須なので入れましょう。

@Component(modules = [AndroidInjectionModule::class, ActivityModule::class])
interface ApplicationComponent {
    fun inject(application: SampleApplication)
}

Applicationクラスをカスタマイズする

カスタムApplicationクラスを作成して、DispatchingAndroidInjectorを注入します。 このHasActivityInjectorを実装して、activityInjcetor()でdispatchingAndroidInjectorをDaggerが取り出せるようにしましょう。 なお、先ほど作ったComponentのinjectを読んでおけばdispatchingAndroidInjectorは注入されます。

class SampleApplication : Application(), HasActivityInjector {

    @Inject
    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>

    override fun onCreate() {
        DaggerApplicationComponent.create().inject(this)
        super.onCreate()
    }

    override fun activityInjector(): AndroidInjector<Activity> {
        return dispatchingAndroidInjector
    }

}

Activityに注入する

あとはAndroidInjection.inject(this)をonCreateで呼べば完成です。 AndroidInjcectionがApplicationクラスからAndroidInjectorを取ってきてinjectしてくれるみたいです。

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var coffeeMaker: CoffeeMaker

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

        findViewById<TextView>(R.id.testText).text = coffeeMaker.getTemperature()
    }
}

とはいえAndroidInjection.inject(this)は毎回書く必要がある?

DroidKaigi アプリをみると。。。書いてない!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setSupportActionBar(binding.toolbar)
        setupNavigation()
        setupStatusBarColors()

        systemStore.errorMsg.changed(this) { message ->
            val messageStr = when (message) {
                is ErrorMessage.ResourceIdMessage -> getString(message.messageId)
                is ErrorMessage.Message -> message.message
            }
            Snackbar.make(binding.root, messageStr, Snackbar.LENGTH_LONG).show()
        }
        userStore.registered.changed(this) { registered ->
            if (!registered) {
                userActionCreator.load()
            }
        }
    }

DaggerApplication, DaggerActivityを継承する

上記継承したクラスだと、今回手動で書いていた大部分をやってくれるらしい。

なので以下のように修正します。

ApplicationComponent

Component(modules = [AndroidInjectionModule::class, ActivityModule::class])
interface ApplicationComponent : AndroidInjector<SampleApplication>  // injectとか取っ払ってAndroidInjector<SampleApplication>を継承する

SampleApplication

class SampleApplication : DaggerApplication() {
    // injectはDaggerApplicationがやってくれるので、AndroidInjectorを渡すだけ。
    override fun applicationInjector(): AndroidInjector<SampleApplication> {
        return DaggerApplicationComponent.builder().build()
    }
}

MainActivity

class MainActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var coffeeMaker: CoffeeMaker // DaggerActivityがinjectしてくれるのでinjectは記載しなくてよい

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<TextView>(R.id.testText).text = coffeeMaker.getTemperature()
    }
}

これでようやくDroidKaigiのDIが見えてきました。

感想

  • この書き方で注入作業もActivityから消えたので、よりonCreateが本質的な処理しか無くなってきている。素敵。
  • そろそろMainActivityから脱出したい。。

サンプルプログラム

github.com