実用的なライブラリを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
まずはライブラリを入れましょう
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"
}
テストでも利用したい場合はこちらも必要です
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クラスに作成することが好ましいと言えます。
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()
}
}
<application
android:name=".CustomApplication"
:
そしてActivityでこのように利用する
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があったとします
class ResourceProvider @Inject constructor(private val context: Context) {
// ...
}
そしてこのResourceProviderに依存するViewModelがあったとします
class TestViewModel @Inject constructor(private val resourceProvider: ResourceProvider) {
// ...
}
このとき、TestViewModelは@Provide
指定する必要はありません。
ではこのTestViewModelを仮にシングルトンにしたい場合、どうすればいいのでしょうか?
class BMIRealtimeCalculateActivity : AppCompatActivity() {
@Inject lateinit var viewModel: TestViewModel
そんなとき、TestViewModelをシングルトンにしたい場合、Classに@Singleton
をつけます。
@Singleton
class TestViewModel @Inject constructor(private val resourceProvider: ResourceProvider) {
// ...
}
Binds
例えばあるinterfaceを実装したクラスがあり、
操作するのはこのinterfaceを通して行いたい、ということがあると思います。
そんなときは @Binds
を指定することで、
Injectの際にインターフェースの形でインスタンスを自動生成することができます
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)
}
}
}
class SessionManager @Inject constructor(private val accountApi: IAccountApi) {
// ...
}
応用
Named
Provideするインスタンスは同じでも、中身を変えたい
そんなときはProvideに名前をつけることができます
@Provides
@Named(value = "F")
fun provideGenderFemale(): Gender {
return Gender.Female
}
@Provides
@Named(value = "M")
fun provideGenderMale(): Gender {
return Gender.Male
}
// 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でちょっと違うので並べて書いておきます
@Scope
@Retention(RetentionPolicy.RUNTIME)
@interface ViewModelScope {
}
@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
@Singleton
@Component(modules = arrayOf(
AppModule::class)
)
interface AppComponent {
fun inject(customApplication: CustomApplication)
fun plus(module: ActivityModule): ActivityComponent
}
@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
@ActivityScope
@Subcomponent(modules = arrayOf(ActivityModule::class))
interface ActivityComponent {
fun inject(activity: BMIRealtimeCalculateActivity)
}
@Module
class ActivityModule(private val mActivity: Activity) {
@Provides
fun provideActivity(): Activity {
return mActivity
}
@ActivityScope
@Provides
fun providePersonPresenter(): PersonPresenter {
return PersonPresenter()
}
}
例)Componet作成
override fun onCreate() {
super.onCreate()
appComponent = DaggerAppComponent.builder()
.appModule(AppModule(this))
.build().also { it.inject(this) }
}
val mComponent: ActivityComponent get() = (application as CustomApplication).getComponent()
.plus(ActivityModule(this))
Android向け
Dagger2にはAndroid向けに
ActivityやFragmentに対してのInjectorが搭載されたサブライブラリがあります。
ただこれも個人的には結構難しいイメージがあり、
それならFragment/Activityの基底クラスを作ってそれぞれのSubComponentを利用すればいいんじゃ。。と思っちゃいます。
NonExistentClass
ビルドをすると、上記エラーが発生するケースがあるかもしれません。
これはKotlinでビルドすると起きがちなものみたいですが、
以下を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がつながっているのであれば
リンクですぐに参照が可能なプラグインが用意されているので、
必要な方は入れてみてもいいかもしれません。