Dagger2 + Android + ViewModel(with databinding)

Posted by on Tue, Oct 17, 2017

はじめに

daggerバージョンが2.10になったときに、dagger.androidがリリースされたようで、いままでと少し書き方が変わっていたので、メモを書きました。

基本的に下記ページを参考にしています。

https://google.github.io/dagger/android.html

またサンプルコードは、Dagger 2.12 時点によるものです。

dagger.android

MainActivityが依存しているインスタンスを注入できるようにしてみます。

  1. AndroidInjectionModule(あるいは、Support componentを使う場合は AndroidSupportInjectionModule)をアプリケーションコンポーネントに指定してください。

    @Singleton
    @Component(modules = arrayOf(
            AndroidSupportInjectionModule::class
        )
    )
    interface AppComponent : AndroidInjector<App> {
    
        @Component.Builder
        interface Builder {
            @BindsInstance
            fun application(application: App): Builder
    
            fun build():AppComponent
    
        }
    }
    
  2. AndroidInjector.Builder<YourActivity>を継承している@Subcomponent.Builderを持つ AndroidInjector<YourActivity> を実装する@Subcomponent を作成します。

    @Subcomponent
    interface MainActivitySubComponent:AndroidInjector<MainActivity> {
        @Subcomponent.Builder
        abstract class Builder : AndroidInjector.Builder<MainActivity>()
    }
    
  3. subcomponentを定義したら、Subcomponent.Builderをバインドするモジュールを定義(ここではMainActivityModule)し、

    @Module(subcomponents = arrayOf(MainActivityModule.MainActivitySubComponent::class))
    abstract class MainActivityModule {
    
        @Binds
        @IntoMap
        @ActivityKey(MainActivity::class)
        internal abstract fun bindAndroidInjectorFactory(
                builder: MainActivityModule.MainActivitySubComponent.Builder): AndroidInjector.Factory<out Activity>
    }
    

    そのモジュール(MainActivityModule)をアプリケーションコンポーネント(ここではAppComponent)に追加します。

    @Singleton
    @Component(modules = arrayOf(
            AndroidSupportInjectionModule::class,
            MainActivityModule::class
        )
    )
    interface AppComponent : AndroidInjector<App> {
    // 省略
    }
    
  4. Appplication クラスを継承したクラス(ここではApp)で HasActivityInjectorインターフェースを実装し、 DispatchingAndroidInjector<Activity> を @Injectてし、 activityInjector() メソッドでそれを返します。

    と、書かれていますが、daggerがBaseクラスを用意してくれていて、

    DaggerApplicationクラスを継承して、 activityInjector()メソッドを実装すると良いみたいです。つまり

    open class App : DaggerApplication() {
        override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
            return  DaggerAppComponent.builder().application(this).build()
        }
    }
    

    とします。

  5. 最後にAcitivyのonCreateでsuper.onCreate()の前にAndroidInjection.inject(this)を呼んでください。

    とあるのですが、これもDaggerが用意してくれているベースクラスDaggerAppCompatActivity を継承すれば、AndroidInjection.inject(this)を書くなくてもDaggerAppCompatActivityでやってくれています。

これで基本は完了です。

ViewModel(databinding)のインスタンスを注入できるようにしてみます。

ViewModelクラスを作成します。

class MainViewModel : BaseObservable() {
    fun onClickButton(view: View) {
        Log.d("MainViewModel", "onclick")
    }
}

このViewModelは下記のactivity_main.xmlにバインドしたいため、次のようにlayoutタグを入れています。

<layout 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"
    >
    <data>
        <variable
            name="viewModel"
            type="net.kwmt27.codesearch.presentation.viewmodel.MainViewModel"
            />
    </data>

    <LinearLayout>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="button"
            android:onClick="@{viewModel::onClickButton}"
            />
    </LinearLayout>
</layout>

なお、ActivityScopeアノテーションは下記のような感じで作成しています。

@Scope
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class ActivityScope

このViewModelをActivityでInjectできるようにするには、コンストラクタに@Injectアノテーションをつけ、Scopeアノテーションを付けます。

@ActivityScope
class MainViewModel @Inject constructor() : BaseObservable() {

    fun onClickButton(view: View) {
        Log.d("MainViewModel", "onclick")
    }

}

このような感じになると思います。そしてInjectしたいActivityのフィールド宣言で@Injectアノテーションを付けます。MainActivityは次のようになります。

class MainActivity : DaggerAppCompatActivity() {
    @Inject
    lateinit var viewModel: MainViewModel
}

あとは通常どおりにバインディングするだけです。

class MainActivity : DaggerAppCompatActivity() {
    @Inject
    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        binding.viewModel = viewModel
    }
}

FragmentでInject可能にする

ほとんどActivityと同じですが、ちょっと書いておきます。

MainFragmentクラスを作成します。

@FragmentScope
class MainFragment : DaggerFragment() {

    companion object Factory {
        val TAG = MainFragment::class.simpleName!!
        fun newInstance(): MainFragment = MainFragment()
    }

    @Inject
    lateinit var viewModel: MainFragmentViewModel

    private lateinit var binding:FragmentMainBinding

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        binding = FragmentMainBinding.inflate(inflater, container, false)
        binding.viewModel = viewModel
        return binding.root
    }
}

このとき、レイアウトにfragment_main.xmlを、ViewModelとしてMainViewModel.ktを、Fragmentスコープ用にFragmentScope.ktを作成しています。それぞれ次のようになっています。

  • fragment_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <layout 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"
        >
        <data>
            <variable
                name="viewModel"
                type="net.kwmt27.codesearch.presentation.viewmodel.MainFragmentViewModel"
                />
        </data>
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            >
            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:onClick="@{viewModel::onClickButton}"
                android:text="Fragment Button"
                />
        </RelativeLayout>
    </layout>
    
  • MainFragmentViewModel.kt

    @FragmentScope
    class MainFragmentViewModel @Inject constructor() : BaseObservable() {
        fun onClickButton(view: View) {
            Log.d("MainFragmentViewModel", "onclick")
        }
    }
    
  • FragmentScope.kt

    @Scope
    @Retention(AnnotationRetention.RUNTIME)
    @MustBeDocumented
    annotation class FragmentScope
    

MainActivityModule.ktと同様にして、MainFragmentModule.ktを作成します。

@Module(subcomponents = arrayOf(MainFragmentModule.MainFragmentSubComponent::class))
abstract class MainFragmentModule {

    @Binds
    @IntoMap
    @FragmentKey(MainFragment::class)
    internal abstract fun bindMainFragmentAndroidInjectorFactory(
            builder: MainFragmentModule.MainFragmentSubComponent.Builder): AndroidInjector.Factory<out Fragment>

    @FragmentScope
    @Subcomponent
    interface MainFragmentSubComponent: AndroidInjector<MainFragment> {
        @Subcomponent.Builder
        abstract class Builder : AndroidInjector.Builder<MainFragment>()
    }

}

これをAppComponent.ktに差し込むだけです。

@Singleton
@Component(modules = arrayOf(
        AndroidSupportInjectionModule::class,
        AppModule::class,
        MainActivityModule::class,
        MainFragmentModule::class // ←ここに入れた
    )
)
interface AppComponent : AndroidInjector<App> {

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: App): Builder

        fun build():AppComponent

    }
}

FragmentをInjectableにする方法は以上です。

最後に

ActivityやFragmentが増えたときに、AppComponentのmodulesが太ってくる問題がありますが、これはまた後のほど考えたいと思います。どこかでincludesみたいなのを見たので、もしかしたら

ActivityModuelsFragmentModulesみたいなのを作ってそこにまとめて書けるのかもしれません。



comments powered by Disqus