Skip to main content

Koject v1.2.0 - Androidコンポーネントの追加

Mori Atsushi

Koject 1.1.0でAndroidのサポートが強化されたばかりですが、Koject 1.2.0ではさらに複数の機能が追加されています。 この記事では、v1.2.0の利用方法と、追加された主な機能について紹介します。

Read in English →

v1.1.0から移行する

v1.1.0からv1.2.0に向け、一部のAPIが変更されました。対応が必要になる可能性があります。

[core] The inject() API for Named has changed (#148)
// Until v1.1.0
val db1 = inject<DB>("db1")
val db2 = inject<DB>("db2")

// Since v1.2.0
val db1 = inject<DB>(Named("db1"))
val db2 = inject<DB>(Named("db2"))
[core] The ComponentExtras API has changed. (#157)
// Until v1.1.0
@ExperimentalKojectApi
@ComponentExtras(CustomComponent::class)
class CustomComponentExtras(
val extra: ExtraClass
)

// Since v1.2.0
@ExperimentalKojectApi
class CustomComponentExtras(
val extra: ExtraClass
): ComponentExtras<CustomComponent>
[android] injectViewModels() has been renamed to lazyViewModels() (#149)
// Until v1.1.0
private val viewModel: TopViewModel by injectViewModels()

// Since v1.2.0
private val viewModel: TopViewModel by lazyViewModels()

Android Applicationを配布する

Koject 1.2.0から、Androidアプリケーションの配布が可能になりました。 koject-android-coreパッケージを追加することで利用できます。

dependencies {
implementation("com.moriatsushi.koject:koject-android-core:1.2.0")
}

利用するには、Koject.start()を呼び出す際にapplication()メソッドでApplicationを渡す必要があります。

class MyApplication : Application() {
override fun onCreate() {
super.onCreate()

Koject.start {
application(this@MyApplication)
}
}
}

これにより、 Applicationクラス及びApplication scopeのContextクラスが配布されるようになります。 配布されたクラスは以下のように利用できます。

@Provides
@Singleton
fun provideAppDatabase(
applicationContext: Context // can be injected
): AppDatabase {
return Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"database-name"
).build()
}

ViewModelのCoroutineScopeを配布する

AndroidのViewModelを使ったアーキテクチャでは、ViewModelが肥大化する傾向になります。 その対策としてViewModelの一部の処理を以下のように別クラスに切り出すことが考えられます。

class MyViewModel: ViewModel() {
private val helper = TopViewModelHelper(viewModelScope)

fun update() {
helper.update()
}
}
class ViewModelHelper(
private val coroutineScope: CoroutineScope
) {
fun update() {
coroutineScope.launch {
/* ... */
}
}
}

DIコンテナを使用して、このViewModelHelperを配布しようとすると、ViewModelCoroutineScopeViewModelHelperに渡すのが難しくなります。

例えばこのようにsetup関数を通じて渡す方法が考えられますが、setup関数を呼び忘れる可能性があり、あまり安全ではありません。

@Provides
class ViewModelHelper {
private lateinit var coroutineScope: CoroutineScope

fun setup(coroutineScope: CoroutineScope) {
this.coroutineScope = coroutineScope
}

fun update() {
coroutineScope.launch {
/* ... */
}
}
}
@Provides
class MyViewModel(
private val helper: TopViewModelHelper
): ViewModel() {
init {
helper.setup(viewModelScope)
}

fun update() {
helper.update()
}
}

他にも、Factoryクラスを配布する方法も考えられます。

class ViewModelHelper(
private val coroutineScope: CoroutineScope
) {
fun update() {
coroutineScope.launch {
/* ... */
}
}

@Provides
class Factory {
fun create(coroutineScope: CoroutineScope): ViewModelHelper {
return ViewModelHelper(coroutineScope)
}
}
}
@Provides
class MyViewModel(
helperFactory: TopViewModelHelper.Factory
): ViewModel() {
private val helper = helperFactory.create(viewMdoelScope)

fun update() {
helper.update()
}
}

どちらの方法も動作はしますが、冗長さが残ります。

Kojectでは、ViewModelComponentを使うことでシンプルに解決することができます。 @Providesアノテーションをつける際に@ViewModelComponentをつけると、@ViewModelCoroutineScopeアノテーションを使用してViewModel ScopeのCoroutineScopeをinjectできるようになります。

@ViewModelComponent
@Provides
class MyViewModel(
private val helper: ViewModelHelper
): ViewModel() {
fun update() {
helper.update()
}
}
@ViewModelComponent
@Provides
class ViewModelHelper(
@ViewModelCoroutineScope
private val coroutineScope: CoroutineScope // same as ViewModel.viewModelScope
) {
fun update() {
coroutineScope.launch {
/* ... */
}
}
}

@ViewModelComponent がついたタイプは、同じ@ViewModelComponent がついたタイプにのみinjectが可能な点に注意してください。 MyViewModelクラスにも@ViewModelComponentアノテーションを付ける必要があります。

ViewModelを利用する際はComponentActivity.lazyViewModels()関数を使用します。

class TopActivity : ComponentActivity() {
private val viewModel: TopViewModel by lazyViewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/* ... */
}
}

この機能は、処理を移譲するだけでなく、ViewModelの処理の一部を共通化したいときにも役に立ちます。

ViewModelのより詳しい説明はInject ViewModels ドキュメントを確認してください。

Androidコンポーネントが追加されました

Koject 1.2.0ではViewModelComponentに加えて@ActivityComponent, @FragmentComponent, @ComposeComponentが使えるようになりました。

@ActivityComponentアノテーションが付与されているタイプは、Activityもしくは同じ@ActivityComponentがついたタイプにのみinjectができます。 @ActivityComponentがついたアノテーションでは、ComponentActivityやActivity scopeのContextCoroutineScope等が利用できます。

@ActivityComponent
class ActivityHelper(
val activity: ComponentActivity, // can be injected
@ActivityContext
val context: Context, // activity's context
@ActivityCoroutineScope
val coroutineScope: CoroutineScope // same as ComponentActivity.lifecycleScope
)

Activityで利用する際はComponentActivity.inject()もしくはComponentActivity.lazyInject()を使用してください。

class MyActivity: ComponentActivity {
val helper: ActivityHelper by lazyInject() // can be injected
}

この機能はViewModelと同様に、クラス分割等のリファクタリングに役立てることができます。

全てのコンポーネントと、それらが利用できるタイプについては、Android components ドキュメントで確認できます。

フィードバックをお待ちしています。

Kojectは基本的な機能はシンプルに保ちつつ、様々な機能追加を行っています。 足りない機能があれば、ぜひIssueから教えて下さい。