(2019年3月1日変更)
バージョン 1.0.2
に対応した記述に変更しました。
はじめに
AndroidアプリにKoinを使ってDIしてみます。
DIがどんなものかというのはこちら。
やりたいこと
ViewModel
に依存するActivity
があって...
+-----------+
| Activity |
+-----+-----+
|
▽
+-----------+
| ViewModel |
+-----------+
ViewModel
からデータを貰うようなアプリを作ります。
+-----------+ +-----------+
| Activity | | ViewModel |
+-----+-----+ +-----+-----+
| |
| greet() |
+--------------->|
|<---------------+
| Hello |
ViewModel
をActivity
に注入するようにします。
プロジェクトへKoinを導入
まずはこれがないと始まりませんので。
dependencies {
// (省略)
implementation 'org.koin:koin-android:1.0.2'
}
実装
ViewModel
class MainViewModel {
fun greet(): String {
return "こんにちわ"
}
}
アプリケーションクラス
startKoin
でIoCコンテナが生成されるみたいです。
コンテナの中身はモジュール単位でロード可能で、Koin DSL の module
を使って生成します。
モジュールが提供するインスタンス(もしくは、インスタンスを返す何か)をfactory
に設定します。
ちなみにfactory
の代わりにsingle
を使うとシングルトンになります。
import org.koin.android.ext.android.startKoin
import org.koin.dsl.module.applicationContext
import org.koin.dsl.module.Module
class Application : android.app.Application() {
override fun onCreate() {
super.onCreate()
// Koinコンテナ生成
startKoin(this, listOf(
this.module
))
}
// Koinモジュール
private val module: Module = module {
factory { MainViewModel() }
}
}
マニフェスト
アプリ起動時のクラスがApplication
になるようにandroid:name
属性を追加します。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.mykoinapp">
<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>
アクティビティ
ViewModelを代入するプロパティを宣言しておいて、by inject()
を付けておきます。
こうしておくと、コンテナから型が一致するインスタンスが注入されます。
Activityにはコンストラクタインジェクションができないのでこうするようです。
import org.koin.android.ext.android.inject
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by inject() // <- これ
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
println("_/_/ ${this.viewModel.greet()}")
}
}
こういう書き方もできます。
private val viewModel by inject<MainViewModel>()
結果
こんな感じでログが表示されました。
Koinのログも出力されていますね。
コンテナからインスタンスが取り出されている(というかfactory
なので生成されている)ことが確認できます。
06-25 04:38:50.089 11318-11318/com.example.mykoinapp I/KOIN: Resolve class[com.example.mykoinapp.MainViewModel] with Factory[class=com.example.mykoinapp.MainViewModel]
06-25 04:38:50.093 11318-11318/com.example.mykoinapp I/KOIN: (*) Created
06-25 04:38:50.094 11318-11318/com.example.mykoinapp 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
が注入されるように修正します。
class MainViewModel(private val greetingRepository: GreetingRepository) {
fun greet(): String {
return this.greetingRepository.greet()
}
}
アプリケーションクラス
モジュールに格納するインスタンスを追加します。
また、ViewModel
のコンストラクタ引数にget()
を指定します。
get()
を指定すると、Koinが引数の型に一致するインスタンスをコンテナから取得して、自動的に注入してくれます。
class Application : android.app.Application() {
override fun onCreate() {
super.onCreate()
startKoin(this, listOf(
this.module
))
}
private val module: Module = module {
factory { GreetingRepository() } // <- これ
factory { MainViewModel(get()) } // <- 引数に `get()` を指定
}
}
結果
こんな感じでログが表示されました。
Repositoryからの値が表示されています。
06-25 04:45:20.797 11556-11556/com.example.mykoinapp I/KOIN: Resolve class[com.example.mykoinapp.MainViewModel] with Factory[class=com.example.mykoinapp.MainViewModel]
06-25 04:45:20.799 11556-11556/com.example.mykoinapp I/KOIN: Resolve class[com.example.mykoinapp.GreetingRepository] with Factory[class=com.example.mykoinapp.GreetingRepository]
06-25 04:45:20.799 11556-11556/com.example.mykoinapp I/KOIN: (*) Created
06-25 04:45:20.799 11556-11556/com.example.mykoinapp I/KOIN: (*) Created
06-25 04:45:20.799 11556-11556/com.example.mykoinapp I/System.out: _/_/ こんにちわ from Repository
依存関係が増えた場合でも簡単にDIできました。
もっと実践的に
「モックを注入できるように」とかあるのと思うので改良してみます。
Repository
インタフェースを定義して、ユニットテストとかでモックへの差し替えを容易にします。
interface GreetingRepositoryContract {
fun greet(): String
}
class GreetingRepository : GreetingRepositoryContract {
override fun greet(): String {
return "こんにちわ from Repository"
}
}
ViewModel
interface MainViewModelContract {
fun greet(): String
}
class MainViewModel(private val greetingRepository: GreetingRepositoryContract) : MainViewModelContract {
override fun greet(): String {
return this.greetingRepository.greet()
}
}
アプリケーションクラス
モジュールをRepository用とViewModel用とに分割して、
モジュールへのインスタンス格納を"as インタフェース
"とします。
こうしておくと、インタフェースが一致するインスタンスがコンテナから出てきます。
もちろん、先述したように、コンストラクタ引数にget()
を渡すと、型の場合と同じく、インタフェースが一致するインスタンスが自動で注入されます。
class Application : android.app.Application() {
override fun onCreate() {
super.onCreate()
startKoin(this, listOf(
this.repositoryModule,
this.viewModelModule
))
}
private val repositoryModule: Module = module {
factory { GreetingRepository() as GreetingRepositoryContract }
}
private val viewModelModule: Module = module {
factory { MainViewModel(get()) as MainViewModelContract }
}
}
モジュールの分割方針は、開発規模や担当者の分担具合によっていろいろあると思います。
この例では処理レイヤーで分割してみました。
アクティビティ
注入するViewModelの型をインタフェースで指定します。
こうすることで、型(=インタフェース)が一致するインスタンスが自動的に注入されます。
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModelContract by inject() // <- 型にインタフェースを指定
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
println("_/_/ ${this.viewModel.greet()}")
}
}
結果
こんな感じでログが表示されました。
変わり映えしませんが、Repositoryからの値が表示されています。
06-25 05:07:38.525 12512-12512/com.example.mykoinapp I/KOIN: Resolve class[com.example.mykoinapp.MainViewModelContract] with Factory[class=com.example.mykoinapp.MainViewModelContract]
06-25 05:07:38.527 12512-12512/com.example.mykoinapp I/KOIN: Resolve class[com.example.mykoinapp.GreetingRepositoryContract] with Factory[class=com.example.mykoinapp.GreetingRepositoryContract]
06-25 05:07:38.527 12512-12512/com.example.mykoinapp I/KOIN: (*) Created
06-25 05:07:38.527 12512-12512/com.example.mykoinapp I/KOIN: (*) Created
06-25 05:07:38.528 12512-12512/com.example.mykoinapp I/System.out: _/_/ こんにちわ from Repository
おわりに
Dagger2の場合と比較して、非常に簡単にDIできました。
どうしてもDagger2を使わなければならない理由がなければKoinの方が簡単で良いんじゃないかなと思います。
たた、コンテナのインスタンス取り出しが動的に解決されるため、「コンテナに追加し忘れた」なんていうのがあると、実行時エラーの可能性があるので気を付けてください。
とりあえずは以上です。