stephanenicolas / toothpick

A scope tree based Dependency Injection (DI) library for Java / Kotlin / Android.
Apache License 2.0
1.12k stars 115 forks source link

ViewModel init call #414

Closed mrgaric closed 4 years ago

mrgaric commented 4 years ago

Hi, I get your sample code for ViewModel

class BackpackViewModel : ViewModel() {
    val backpack: Backpack by inject()
}

and add an initialization block like here

class BackpackViewModel : ViewModel() {
    val backpack: Backpack by inject()
    init {
        backpack.doSomething()
    }
}

But I got this exception

java.lang.IllegalStateException: The dependency has not be injected yet.

If i call backpack.doSomething() after init call everything is working.

How I fix it?

afaucogney commented 4 years ago

The injection is done in the activity. So, when the vm init happened, the injection by the activity is not yet done.

Either 1/ you have to inject dependencies in the vm init before using the dependency, either 2/ request the action from the activity, when it is ready.

I prefer the 1/, and even from a general purpose, I prefer handing domain work from vm w/o dependency from view, to respect separation of concern !

dlemures commented 4 years ago

@afaucogney is right (thx for the answer!), creation happens before injection. The process is divided into 2 steps:

  1. When you use installViewModelBinding on your scope, we create the viewmodel using the right APIs under the hood.
  2. When you start the injection, for example calling KTP...inject() inside your activity, it's when we inject the dependencies of the viewmodel.

The issue is that you are trying to access to those dependencies on the first step, when they are not ready yet. This is similar to when you wanna access injected fields inside a constructor. If you are curious, I could explain more about the reasons why we designed it that way.

Nevertheless, there is a way to solve it: using a ViewModel Factory to create the viewmodel using Toothpick.

val scope = KTP.openScopes(xxx)
scope.installViewModelBinding<MyViewModel>(activity, InjectedViewModelProvider(scope))
...

// This provider can be reused for any ViewModel
class InjectedViewModelProvider(private val scope: Scope) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return scope.getInstance(modelClass)
    }
}

class BackpackViewModel(private val backpack: Backpack) : ViewModel() {
    init {
        backpack.doSomething()
    }
}

By doing so, you can use constructor and field injection within your viewmodel.

Same, if you are curious, I can provide more info about why we don't provide that functionality out-of-the-box.

dlemures commented 4 years ago

@mrgaric If it works for you, plz feel free to close the issue.

mrgaric commented 4 years ago

thx for the answer. Yes, I am interested, why don't you provide this functionality out of the box?

dlemures commented 4 years ago

So, we provide 2 ways of binding view models to your scope: with and without factory. If we use that constructor injection factory by default for the cases when you don't provide one (as you cannot have 2), then the behavior would not be consistent:

As I said, we considered that it would be inconsistent and in order to understand it, you would need to go deep on how TP does VM injection.

Maybe we should provide the Factory within TP anyway so people can use it if they want to? Or at least provide the example on the WIKI? What do you think?

mrgaric commented 4 years ago

I think an example will be enough. thx.