Android lifecycle library ViewModel using dagger 2

Viewed 32054

I have a ViewModel class just like the one defined in the Connecting ViewModel and repository section of Architecture guide. When I run my app I get a runtime exception. Does anyone know how to get around this? Should I not be injecting the ViewModel? Is there a way to tell the ViewModelProvider to use Dagger to create the model?

public class DispatchActivityModel extends ViewModel {

    private final API api;

    @Inject
    public DispatchActivityModel(API api) {
        this.api = api;
    }
}

Caused by: java.lang.InstantiationException: java.lang.Class has no zero argument constructor at java.lang.Class.newInstance(Native Method) at android.arch.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.java:143) at android.arch.lifecycle.ViewModelProviders$DefaultFactory.create(ViewModelProviders.java:143)  at android.arch.lifecycle.ViewModelProvider.get(ViewModelProvider.java:128)  at android.arch.lifecycle.ViewModelProvider.get(ViewModelProvider.java:96)  at com.example.base.BaseActivity.onCreate(BaseActivity.java:65)  at com.example.dispatch.DispatchActivity.onCreate(DispatchActivity.java:53)  at android.app.Activity.performCreate(Activity.java:6682)  at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118)  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2619) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2727)  at android.app.ActivityThread.-wrap12(ActivityThread.java)  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1478)  at android.os.Handler.dispatchMessage(Handler.java:102)  at android.os.Looper.loop(Looper.java:154)  at android.app.ActivityThread.main(ActivityThread.java:6121)

7 Answers

What may not be obvious in the question is that the ViewModel cannot be injected that way because the ViewModelProvider default Factory that you get from the

ViewModelProvider.of(LifecycleOwner lo) 

method with only the LifecycleOwner parameter can only instantiate a ViewModel that has a no-arg default constructor.

You have a param: 'api' in your constructor:

public DispatchActivityModel(API api) {

In order to do that you need to create a Factory so that you can tell it how to create itself. The sample code from google gives you Dagger config and Factory code as mentioned in the accepted answer.

DI was created to avoid use of the new() operator on dependencies because if the implementations change, every reference would have to change as well. The ViewModel implementation wisely uses a static factory pattern already with ViewProvider.of().get() which makes its injection unnecessary in the case of no-arg constructor. So in the case you don't need to write the factory you don't need to inject a factory of course.

The default ViewModel factory used to get an instance of your DispatchActivityModel in your view constructs the ViewModels using assumed empty constructors.

You can write your custom ViewModel.Factory to get around it, but you'd stil need to take care of completing the dependency graph yourself if you want to provide your API class.

I wrote a small library that should make overcoming this common more straightforward and way cleaner, no multibindings or factory boilerplate needed, while also giving the ability to further parametrise the ViewModel at runtime: https://github.com/radutopor/ViewModelFactory

@ViewModelFactory
public class DispatchActivityModel extends ViewModel {

    private final API api;

    public DispatchActivityModel(@Provided API api) {
        this.api = api;
    }
}

In the view:

public class DispatchActivity extends AppCompatActivity {
    @Inject
    private DispatchActivityModelFactory2 viewModelFactory;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        appComponent.inject(this);

        DispatchActivityModel viewModel = ViewModelProviders.of(this, viewModelFactory.create())
            .get(UserViewModel.class)
    }
}

Like I mentioned, you can also easily add runtime parameters to your ViewModel instances as well:

@ViewModelFactory
public class DispatchActivityModel extends ViewModel {

    private final API api;
    private final int dispatchId;

    public DispatchActivityModel(@Provided API api, int dispatchId) {
        this.api = api;
        this.dispatchId = dispatchId;
    }
}

public class DispatchActivity extends AppCompatActivity {
    @Inject
    private DispatchActivityModelFactory2 viewModelFactory;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        appComponent.inject(this);

        final int dispatchId = getIntent().getIntExtra("DISPATCH_ID", -1);
        DispatchActivityModel viewModel = ViewModelProviders.of(this, viewModelFactory.create(dispatchId))
            .get(UserViewModel.class)
    }
}

Recently I've found another elegant solution for this problem.

My fragment with injected ViewModel looks like:

class SettingsFragment : Fragment() {

    private val viewModel by viewModels(DI::settingsViewModel)
}

To achieve this I've created custom by viewModels delegate:

inline fun <reified VM : ViewModel> Fragment.viewModels(
    crossinline viewModelProducer: () -> VM
): Lazy<VM> {
    return lazy(LazyThreadSafetyMode.NONE) { createViewModel { viewModelProducer() } }
}


inline fun <reified VM : ViewModel> Fragment.createViewModel(
    crossinline viewModelProducer: () -> VM
): VM {
    val factory = object : ViewModelProvider.Factory {
        @Suppress("UNCHECKED_CAST")
        override fun <VM : ViewModel> create(modelClass: Class<VM>) = viewModelProducer() as VM
    }
    val viewModelProvider = ViewModelProvider(this, factory)
    return viewModelProvider[VM::class.java]
}

This property delegate expects lambda function that could create ViewModel instance as argument.

And we can provide this lambda using Dagger2 like this:

@Component(
    modules = [MyModule::class]
)
interface MyComponent {
    //1) Declare function that provides our ViewModel in Component
    fun settingsViewModel(): SettingsViewModel
}


@Module
abstract class MyModule {
    @Module
    companion object {

        //2) Create provides method than provides our ViewModel in Module
        @Provides
        @JvmStatic
        fun provideSettingsViewModel(
            ... // Pass your ViewModel dependencies
        ): SettingsViewModel {
            return SettingsViewModel(
                ...// Pass your ViewModel dependencies
            )
        }
    }
}

// 3) Build Component somewhere, for example in singleton-object.
object DI {

    private var component: MyComponent by lazy {
        MyComponent.builder().build()
    }

    // 4) Declare method that delegates ViewModel creation to Component
    fun settingsViewModel() = component.settingsViewModel()
}

And finally we can pass DI.settingsViewModel() method reference to our delegate in fragment:

private val viewModel by viewModels(DI::settingsViewModel)
Related