This document covers special considerations and patterns useful for the android platform.
Android has many classes where you don't control the constructions of (Activity
,Service
, etc.). You may have noticed
that kotlin-inject only supports constructor injection, so what do you do?
You can pull out the dependencies and related code into its own class and inject that instead. You may find this makes it easier to test as well.
instead of:
class MyActivity : Activity() {
@Inject
lateinit var imageLoader: ImaegLoader
@Inject
lateinit var analytics: Analytics
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate()
setContentView(R.layout.my_layout)
// ...
imageLoader.loadImage(imageView, "https://...")
}
override fun onResume() {
super.onResume()
analytics.screenView("My Screen")
}
}
do:
@Inject
class MyScreen(private val imageLoader: ImageLoader, private val analytics: Analytics) {
fun loadImage(imageView: ImageView) {
imageLoader.loadImage(imageView, "https://...")
}
fun onResume() {
analytics.screenView("My Screen")
}
}
@Component
abstract class ActivityComponent(@Component val parent: ApplicationComponent) {
abstract val myScreen: MyScreen
}
class MyActivity : Activity() {
private lateinit var myScreen: MyScreen
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate()
myScreen = ActivityComponent::class.create(ApplicationComponent.getInstance(this)).myScreen
setContentView(R.layout.my_layout)
// ...
myScreen.loadImage(imageLoader)
}
override fun onResume() {
super.onResume()
myScreen.onResume()
}
}
For fragments, you can do one better. You can use constructor injection by providing a custom FragmentFactory.
@Inject
class InjectFragmentFactory(
private val homeFragment: () -> HomeFragment,
private val settingsFragment: () -> SettingsFragment,
) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when (className) {
name<HomeFragment> -> homeFragment()
name<SettingsFragment> -> settingsFragment()
else -> super.instantiate(classLoader, className)
}
}
private inline fun <reified C> name() = C::class.qualifiedName
}
@Component
abstract class MainActivityComponent(@Component val parent: ApplicationComponent) {
abstract val fragmentFactory: InjectFragmentFactory
}
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory =
MainActivityComponent::class.create(ApplicationComponent.getInstance(this))
.fragmentFactory
super.onCreate(savedInstanceState)
}
}
@Inject
class HomeFragment(private val imageLoader: ImageLoader) : Fragment()
@Inject
class SettingsFragment(private val imageLoader: ImageLoader) : Fragment()
ViewModels need special care when constructing so that they can be retained across configuration changes. To do this, you can inject a function that creates the ViewModel instead of the ViewModel directly.
@Inject
class HomeViewModel(private val repository: HomeRepository) : ViewModel()
@Inject
class HomeFragment(homeViewModel: () -> HomeViewModel) : Fragment() {
private val viewModel by viewModels<HomeViewModel> {
object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = homeViewModel() as T
}
}
}
or if you want to use SavedStateHandle
@Inject
class HomeViewModel(private val repository: HomeRepository, handle: SavedStateHandle) : ViewModel()
@Inject
class HomeFragment(homeViewModel: (SavedStateHandle) -> HomeViewModel) : Fragment() {
private val viewModel by viewModels<MainViewModel> {
object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T = homeViewModel(handle) as T
}
}
}
You may want to create helper functions for these, or use injectedvmprovider which provides them for you.
import androidx.fragment.app.viewModels
inline fun <reified VM : ViewModel> Fragment.viewModels(crossinline factory: () -> VM): Lazy<VM> =
viewModels {
object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = factory() as T
}
}
inline fun <reified VM : ViewModel> Fragment.viewModels(crossinline factory: (SavedStateHandle) -> VM): Lazy<VM> =
viewModels {
object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T = factory(handle) as T
}
}
@Inject
class HomeFragment(homeViewModel: () -> HomeViewModel) : Fragment() {
private val viewModel by viewModels(homeViewModel)
}
@Inject
class HomeFragment(homeViewModel: (SavedStateHandle) -> HomeViewModel) : Fragment() {
private val viewModel by viewModels(homeViewModel)
}
You may want to provide different dependencies based on the build-type/flavor. You can do this by splitting those out
into a VariantComponent
interface that's declared in each variant.
// debug/java/com.example.inject/VariantComponent.kt
interface VariantComponent {
val DebugClient.bind: Client
@Provides get() = this
}
// release/java/com.example.inject/VariantComponent.kt
interface VariantComponent {
val ReleaseClient.bind: Client
@Provides get() = this
}
// main/java/com.example.inject/ApplicationComponent.kt
@Component
abstract class ApplicationComponent : VariantComponent