暑中お見舞い申し上げます。
ビールが美味しい季節ですね。
いこーよのAndroidアプリを担当しているhondaです。
この夏、koinを始めました。
koinとは?
Kotlinのために作られたDI(依存性の注入)を実現するための軽量フレームワークです。 github.com
DI(依存性の注入)とは?
以下のようなクラスがあったとします。
public SampleNumCounter { private val sampleNumDataSource: SampleNumDataSource [f:id:kou_hon:20180821105751j:plain] init { this.sampleNumDataSource = SampleNumDataSource() } fun countSampleNum() { this.sampleNumDataSource.sampleNum++ } }
SampleNumCounterはSampleNumDataSourceに依存しています。
SampleNumCounterにDI(依存性の注入)を適用すると下記のようになります。
public SampleNumCounter(private val sampleNumDataSource: SampleNumDataSource) { fun countSampleNum() { this.sampleNumDataSource.sampleNum++ } }
SampleNumCounterのコンストラクタで依存するSampleNumDataSourceを注入するようにしました。
これでSampleNumDataSourceをモックすることができ、SampleNumCounterがTestableになりました。
サンプルアプリを用意しました。
ボタンをクリックする事に数字をインクリメントして画面に表示するアプリを作りました。
まず画面となるActivityクラスです。
class MainActivity : AppCompatActivity() { private lateinit var mainViewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) this.mainViewModel = ViewModelProviders .of( this, MainViewModelFactory( sampleNumCounter = SampleNumCounter( sampleNumDataSource = SampleNumDataSource() ) ) ) .get(MainViewModel::class.java) this.mainViewModel.sampleNumStringLiveData.observe(this, Observer { this.activityMainSampleTextView.text = it }) this.activityMainSampleButton.setOnClickListener { this.mainViewModel.countSampleNum() } } }
MainViewModelにパラメータを渡すために必要になるFactoryクラスです。
class MainViewModelFactory( private val sampleNumCounter: SampleNumCounterInterface ) : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return MainViewModel(sampleNumCounter = this.sampleNumCounter) as T } }
ActivityクラスとSampleNumCounterクラスとの仲立ちをするViewModelクラスです。
class MainViewModel(val sampleNumCounter: SampleNumCounterInterface) : ViewModel() { val sampleNumStringLiveData = MutableLiveData<String>() fun countSampleNum() { this.sampleNumCounter.countSampleNum() this.sampleNumStringLiveData.value = this.sampleNumCounter.getSampleNumString() } }
数字のインクリメントと出力を担当するSampleNumCounterクラスとそのinterfaceです。
interface SampleNumCounterInterface { fun countSampleNum() fun getSampleNumString(): String }
class SampleNumCounter( private val sampleNumDataSource: SampleNumDataSourceInterface ) : SampleNumCounterInterface { override fun countSampleNum() { this.sampleNumDataSource.sampleNum++ } override fun getSampleNumString(): String { return "SampleNum is ${this.sampleNumDataSource.sampleNum}" } }
数字を保持するDataStoreクラスとそのinterfaceです。
interface SampleNumDataSourceInterface { var sampleNum: Int }
class SampleNumDataSource: SampleNumDataSourceInterface { override var sampleNum: Int = 0 }
koinを始めてみましょう
以下の設定でkoinを使えるようになります。
build.gradle(project)にjcenter()
が記述されているかを確認します。
buildscript { repositories { jcenter() } }
build.gradle(module)のdependenciesに以下を追加します
※下記に記述されているkoin_versionは2018/08/06付現在の最新バージョンです。
def koin_version = '0.9.3' // Koin for Kotlin implementation "org.koin:koin-core:$koin_version" // Koin for Android implementation "org.koin:koin-android:$koin_version" // Koin for Android Architecture Components implementation "org.koin:koin-android-architecture:$koin_version" // Koin for JUnit tests testImplementation "org.koin:koin-test:$koin_version"
koinを使ってみましょう
まずkoinは依存定義をApplicationクラスを継承したクラス内で行います。
class KoinSamleApplication : Application() { override fun onCreate() { super.onCreate() // startKoin()でkoinを経由したDIが使用できるようになります。 startKoin(this, listOf(myModule)) } } // applicationContext関数を使って依存関係を定義します。 // factoryで依存定義されたSampleNumCounterは呼び出される度に新しいインスタントが生成されます。 // get()でSampleNumCounterが依存するクラスのインスタンスが挿入されます。 // beanで依存定義されたSampleNumDataSourceはシングルトンとして生成されます。 val myModule: Module = applicationContext { factory { SampleNumCounter(get()) } bean { SampleNumDataSource() as SampleNumDataSourceInterface } }
SampleNumCounterクラスに依存しているMainViewModelFactoryクラスを書き直します。
class MainViewModelFactory : ViewModelProvider.Factory, KoinComponent { // 下記でkoinが変数sampleNumCounterにSampleNumCounterのインスタンスをインジェクトしてくれます // Activity以外でinjectを使う場合はKoinComponentを適用すると使用できます。 private val sampleNumCounter by inject<SampleNumCounter>() override fun <T : ViewModel?> create(modelClass: Class<T>): T { return MainViewModel(sampleNumCounter = this.sampleNumCounter) as T } }
koinはViewModelにも対応しています。 まずKoinSamleApplicationクラスの依存定義を書き直します。
class KoinSamleApplication : Application() { override fun onCreate() { super.onCreate() startKoin(this, listOf(myModule)) } } val myModule: Module = applicationContext { // viewModelでMainViewModelの依存定義を追加します。 viewModel { MainViewModel(get()) } factory { SampleNumCounter(get()) as SampleNumCounterInterface } bean { SampleNumDataSource() as SampleNumDataSourceInterface } }
次にMainActivityを書き直します。
class MainActivity : AppCompatActivity() { // 下記で変数mainViewModelにMainViewModelのインスタンスがインジェクトされます。 // これによりonCreateで記述してたViewModelProvidersの処理とMainViewModelFactoryクラスが必要なくなります。 private val mainViewModel by viewModel<MainViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) this.mainViewModel.sampleNumStringLiveData.observe(this, Observer { this.activityMainSampleTextView.text = it }) this.activityMainSampleButton.setOnClickListener { this.mainViewModel.countSampleNum() } } }
まとめ
いかがでしたでしょうか?
koinを使うことによって、DI定義がすっきりしました。
Android Architecture ComponentのViewModelの取り扱いも楽になりました。
koinはKotlinで書かれているのでKotlinとの相性も抜群です。
今回使用したAndroid Architecture ComponentのViewModelはandroid.arch.lifecycleのものです。
androidxは現時点では対応していないようです。
ですが、バージョン1.0.0のブランチを見るとAndroidXに対応する記述があるので一安心です。
github.com
最後に
We're Hiring Android Engineers! actindi.net