While Android support was recently strengthened in Koject 1.1.0, several more features have been added in Koject 1.2.0. In this article, I'll introduce how to use v1.2.0 and the main features that have been added.
Migrating from v1.1.0
Some APIs have been changed from v1.1.0 to v1.2.0, which may require your attention.
[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()
Providing Android Applications
With Koject 1.2.0, it's now possible to provide the Android Application
class.
This can be done by adding the koject-android-core
package.
dependencies {
implementation("com.moriatsushi.koject:koject-android-core:1.2.0")
}
To use it, you need to pass an Application
using the application()
method when calling Koject.start()
.
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Koject.start {
application(this@MyApplication)
}
}
}
This allows the Application
class and the Context
class in the Application scope to be provided.
The provided classes can be used like this:
@Provides
@Singleton
fun provideAppDatabase(
applicationContext: Context // can be injected
): AppDatabase {
return Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"database-name"
).build()
}
Inject ViewModel's CoroutineScope
In the Android architecture using ViewModels, it is common for the ViewModel class to become bloated with code. One solution is to extract some of the code into a separate class, as shown below:
class MyViewModel: ViewModel() {
private val helper = TopViewModelHelper(viewModelScope)
fun update() {
helper.update()
}
}
class ViewModelHelper(
private val coroutineScope: CoroutineScope
) {
fun update() {
coroutineScope.launch {
/* ... */
}
}
}
When attempting to provide this ViewModelHelper
using a DI container, it becomes difficult to pass the CoroutineScope
of the ViewModel
to the ViewModelHelper
.
One possible solution is to pass it through a setup
function, as shown below.
However, there is a risk of forgetting to call the setup
function, making this approach less safe.
@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()
}
}
Another possible solution is to provide a Factory
class.
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()
}
}
Both methods work, but there is still redundancy.
With Koject, this can be solved simply by using the ViewModelComponent
.
When the @Provides
annotation is accompanied by the @ViewModelComponent
annotation, the CoroutineScope
of the ViewModel Scope can be injected using the @ViewModelCoroutineScope
annotation.
@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 {
/* ... */
}
}
}
Note that types with the @ViewModelComponent
annotation can only be injected into other types with the same annotation.
The MyViewModel
class must also have the @ViewModelComponent
annotation.
To use ViewModel, use the ComponentActivity.lazyViewModels()
function.
class TopActivity : ComponentActivity() {
private val viewModel: TopViewModel by lazyViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/* ... */
}
}
This functionality is useful not only for delegating work to other classes, but also for consolidating parts of ViewModel code.
For more information about ViewModels, please refer to the Inject ViewModels documentation.
Added Android components
Android components have been added to Koject 1.2.0, which now includes @ActivityComponent
, @FragmentComponent
, and @ComposeComponent
, in addition to ViewModelComponent
.
Types annotated with @ActivityComponent
can only be injected into an Activity
or a type with the same annotation.
@ActivityComponent
allows you to use ComponentActivity
, activity scope Context
, CoroutineScope
, and more.
@ActivityComponent
class ActivityHelper(
val activity: ComponentActivity, // can be injected
@ActivityContext
val context: Context, // activity's context
@ActivityCoroutineScope
val coroutineScope: CoroutineScope // same as ComponentActivity.lifecycleScope
)
When using it in an activity, please use ComponentActivity.inject()
or ComponentActivity.lazyInject()
.
class MyActivity: ComponentActivity {
val helper: ActivityHelper by lazyInject() // can be injected
}
Similar to ViewModel, this function can be useful for refactoring such as class division.
You can check all components and the types they can be used with in the Android components documentation.
What more do you need?
Koject aims to keep its basic functions simple while adding various features. If you find any missing functionality, please let us know via Issues.