AndroidアプリにDagger2を使ってDIしてみます。
DIがどんなものかというのはこちら。
Daggerの他にもDIフレームワークがあります。
Koinを使ったDIはこちら。
やりたいこと
ViewModel
に依存するActivity
があって...
+-----------+
| Activity |
+-----+-----+
|
▽
+-----------+
| ViewModel |
+-----------+
ViewModel
からデータを貰うようなアプリを作ります。
+-----------+ +-----------+
| Activity | | ViewModel |
+-----+-----+ +-----+-----+
| |
| greet() |
+--------------->|
|<---------------+
| Hello |
ViewModel
をActivity
に注入するようにします。
プロジェクトへDagger2を導入
まずはこれがないと始まりませんので。
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できてるようです。
06-25 00:59:09.107 3575-3575/com.example.mydaggerapp I/System.out: _/_/ こんにちわ
依存関係を追加するとどうなるか
ViewModel
がRepository
に依存するようにして、その Repository
をViewModel
に注入してみようと思います。
こんな感じです。
+-----------+ +-----------+ +-------------+
| 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)
}
}
コンポーネント
modules
にRepositoryModule
クラスを追加します。
@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まで渡ってきています。
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
属性を追加します。
<?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()}")
}
}
結果
変わり映えしませんが、こんな感じでログが表示されました。
意図した通りに動いているようです。
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・ファインマン
とりあえずは以上です。