All you need for Dependency Injection is Kotlin

You may wonder what is the best Dependency Injection library for your Android project. Dagger, Hilt, Koin, or Kodein? I can’t answer that. You should choose what fits your project, experience, and expectation. But what if I would tell you that you don’t need any library? You have everything that you need in Kotlin! You won’t have some fancy stuff like scoping out of the box, but in most of the projects, you don’t use it also. Of course, you can still work on your approach to scoping in the future.

The simplest DI is the default parameters values in the constructor.

Look how simple is that:

class FancyViewModel(
    private val fancyDataRepository: FancyDataRepository = FancyDataRepository(),
    private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(),
    private val notificationManager: NotificationManager =
        Graph.appContext
        .getSystemService(Context.NOTIFICATION_SERVICE) 
        as NotificationManager,
) : ViewModel() {
class FancyFragment : Fragment() {
    private val fancyViewModel: FancyViewModel by viewModels()

You could see that there is Graph object, but this is also a dead-simple main graph by Kotlin.
You can create as many graph objects as you need, for networking, databases. Organize it in the same way as would you do in all DI libraries.

object Graph {

    private lateinit var appContext: Application
    
    // lazily initialized singleton scope
    val fancyManager by lazy { FancyManager(appContext) }

    fun init(appContext: Application) {
        this.appContext = appContext
    }
}
class FancyApp : Application() {

    override fun onCreate() {
        super.onCreate()
        Graph.init(this)
    }

You can also want to pass some object, that is not a singleton, nor a new instance, to a ViewModel like another ViewModel. For that, you can write your own extension by viewModels with a custom factory for ViewModels:

@Suppress("UNCHECKED_CAST")
class ViewModelFactory<VM : ViewModel>(
    private val creator: () -> VM
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>): T = creator() as T
}

inline fun <reified VM : ViewModel> Fragment.viewModels(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this },
    noinline customCreator: (() -> VM)? = null
): Lazy<VM> =
    viewModels(ownerProducer = ownerProducer, factoryProducer = customCreator?.let { { ViewModelFactory(it) } })

Usage:

private val OtherViewModel: OtherViewModel by viewModels {
    OtherViewModel(fancyViewModel)
}

class OtherViewModel(
    private val fancyViewModel: FancyViewModel,
    private val dispatchers: DispatchersProvider = DefaultDispatchersProvider(),
) : ViewModel() {

The same approach will work for other types of ViewModel delegates:

inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
    noinline customCreator: (() -> VM)? = null
): Lazy<VM> =
    viewModels(factoryProducer = customCreator?.let { { ViewModelFactory(it) } })

inline fun <reified VM : ViewModel> Fragment.activityViewModels(
    noinline customCreator: (() -> VM)? = null
): Lazy<VM> =
    activityViewModels(factoryProducer = customCreator?.let { { ViewModelFactory(it) } })

inline fun <reified VM : ViewModel> Fragment.navGraphViewModels(
    @IdRes navGraphId: Int,
    noinline customCreator: (() -> VM)? = null
): Lazy<VM> =
    navGraphViewModels(navGraphId, factoryProducer = customCreator?.let { { ViewModelFactory(it) } })

And that’s it. This is most all that you need as a Dependency Injection in your project

Ok, there is something more that you could want to use – SavedStateHandle. If yes, then better is a small different approach. Instead of overriding delegates, we can provide functions to provide ViewModel factories.

internal fun <VM : ViewModel> viewModelFactory(
    create: () -> VM
): ViewModelProvider.Factory {
    return object : ViewModelProvider.Factory {
        @Suppress("UNCHECKED_CAST")
        override fun <VM : ViewModel> create(modelClass: Class<VM>): VM  = create() as VM
    }
}

internal fun <VM : ViewModel> Fragment.savedStateViewModelFactory(
    create: (savedStateHandle: SavedStateHandle) -> VM
): ViewModelProvider.Factory {
    return object : AbstractSavedStateViewModelFactory(this, arguments) {
        @Suppress("UNCHECKED_CAST")
        override fun <VM : ViewModel?> create(
            key: String, modelClass: Class<VM>, 
            handle: SavedStateHandle
        ): VM {
            return create(handle) as VM
        }
    }
}

It will work with the default ViewModel delegates:

private val viewModel: FancyViewModel by viewModels {
    viewModelFactory {
        FancyViewModel()
    }
}

private val savedStateViewModel: FancySavedStateViewModel by viewModels {
    savedStateViewModelFactory { savedState ->            
        FancySavedStateViewModel(savedState)
    }
}

Tell me what do you think about these approaches? Which one is better for you?
What do you use as a DI in your projects?

Join me on The Code Side!
All the best,
Artur

Add a Comment

Your email address will not be published.