Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inject ViewModel using Dagger 2 + Kotlin + ViewModel

class SlideshowViewModel : ViewModel() {

@Inject lateinit var mediaItemRepository : MediaItemRepository

fun init() {
    What goes here?
}

So I'm trying to learn Dagger2 so I can make my apps more testable. Problem is, I've already integrated Kotlin and am working on the Android Architectural components. I understand that constructor injection is preferable but this isn't possible with ViewModel. Instead, I can use lateinit in order to inject but I'm at a loss to figure out how to inject.

Do I need to create a Component for SlideshowViewModel, then inject it? Or do I use the Application component?

gradle:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

kapt { 
    generateStubs = true
}
dependencies {
    compile "com.google.dagger:dagger:2.8"
    annotationProcessor "com.google.dagger:dagger-compiler:2.8"
    provided 'javax.annotation:jsr250-api:1.0'
    compile 'javax.inject:javax.inject:1'
}

Application Component

@ApplicationScope
@Component (modules = PersistenceModule.class)
public interface ApplicationComponent {

    void injectBaseApplication(BaseApplication baseApplication);
}

BaseApplication

    private static ApplicationComponent component;

    @Override
    public void onCreate() {
        super.onCreate();

        component = DaggerApplicationComponent
                .builder()
                .contextModule(new ContextModule(this))
                .build();
        component.injectBaseApplication(this);
    }

    public static ApplicationComponent getComponent() {
        return component;
    }
like image 943
easycheese Avatar asked Jun 23 '17 02:06

easycheese


People also ask

How do you inject a ViewModel with a dagger?

Setup a Dagger 2 Module Then setup the Dagger 2 Module with that provide the ViewModel. Like normal ViewModel creation, use ViewModelProvider and the Factory to create it. We'll need to pass in the dependencies which can be automatically injected by Dagger 2 using the conventional approach.

How do you inject activity into a ViewModel?

To use Activity to use the Component to inject into the ViewModel, sample code below. But if we do add new ViewModels, we don't need new classes for Factory. We don't need a new Component too. We just need to add the new ViewModel class that's it.

How do you inject ViewModel in activity using hilt?

Inject ViewModel objects with Hilt Provide a ViewModel by annotating it with @HiltViewModel and using the @Inject annotation in the ViewModel object's constructor. Note: To use Dagger's assisted injection with ViewModels, see the following Github issue.

How do you use ViewModels?

There are three steps to setting up and using a ViewModel: Separate out your data from your UI controller by creating a class that extends ViewModel. Set up communications between your ViewModel and your UI controller. Use your ViewModel in your UI controller.

Can we inject dependencies in the ViewModel constructor using dagger2?

Now, this solved the problem of the parametrized ViewModel constructor, which means we can use Dagger2 to inject dependencies in the ViewModel constructor. Our ViewModelFactory class implementation works perfectly, but is it an optimized solution? Probably not because we’ve created a new ViewModelFactory class for each ViewModel in our project.

How to create a ViewModel in Dagger 2?

Then setup the Dagger 2 Module with that provide the ViewModel. Like normal ViewModel creation, use ViewModelProvider and the Factory to create it. We’ll need to pass in the dependencies which can be automatically injected by Dagger 2 using the conventional approach. The only special thing here is activity: ComponentActivity.

How to create a ViewModel in Android components without dependency injection?

Have a look: Without using any dependency injection logic, we can create a ViewModel instance object in Android components by providing the ViewModel class object to ViewModelProvider. This will generate the ViewModel instance binding to the context passed in the ViewModelProvider constructor.

How to make the viewmodelfactory class generic in Android?

The IntoMap API allows us to create a map of objects that can be injected into Android components. To make our ViewModelFactory class generic, we need to replace the ViewModel in the constructor with a MutableMap-Class<? extends ViewModel> as the key and ViewModel as the value ( MutableMap<Class<? extends ViewModel>, ViewModel> ).


3 Answers

You can enable constructor injection for your ViewModels. You can check out Google samples to see how to do it in Java. (Update: looks like they converted the project to Kotlin so this URL no longer works)

Here is how to do a similar thing in Kotlin:

Add ViewModelKey annotation:

import android.arch.lifecycle.ViewModel

import java.lang.annotation.Documented
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

import dagger.MapKey
import kotlin.reflect.KClass

@Suppress("DEPRECATED_JAVA_ANNOTATION")
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

Add ViewModelFactory:

import android.arch.lifecycle.ViewModel
import android.arch.lifecycle.ViewModelProvider

import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton

@Singleton
class ViewModelFactory @Inject constructor(
    private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        var creator: Provider<out ViewModel>? = creators[modelClass]

        if (creator == null) {
            for ((key, value) in creators) {
                if (modelClass.isAssignableFrom(key)) {
                    creator = value
                    break
                }
            }
        }

        if (creator == null) {
            throw IllegalArgumentException("unknown model class " + modelClass)
        }

        try {
            return creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }
}

Add ViewModelModule:

import dagger.Module
import android.arch.lifecycle.ViewModel
import dagger.multibindings.IntoMap
import dagger.Binds
import android.arch.lifecycle.ViewModelProvider
import com.bubelov.coins.ui.viewmodel.EditPlaceViewModel

@Module
abstract class ViewModelModule {
    @Binds
    @IntoMap
    @ViewModelKey(EditPlaceViewModel::class) // PROVIDE YOUR OWN MODELS HERE
    internal abstract fun bindEditPlaceViewModel(editPlaceViewModel: EditPlaceViewModel): ViewModel

    @Binds
    internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}

Register your ViewModelModule in your component

Inject ViewModelProvider.Factory in your activity:

@Inject lateinit var modelFactory: ViewModelProvider.Factory
private lateinit var model: EditPlaceViewModel

Pass your modelFactory to each ViewModelProviders.of method:

model = ViewModelProviders.of(this, modelFactory)[EditPlaceViewModel::class.java]

Here is the sample commit which contains all of the required changes: Support constructor injection for view models

like image 146
Igor Bubelov Avatar answered Oct 23 '22 13:10

Igor Bubelov


Assuming you have a Repository class that can be injected by Dagger and a MyViewModel class that has a dependency on Repository defined as such:


    class Repository @Inject constructor() {
       ...
    }

    class MyViewModel @Inject constructor(private val repository: Repository) : ViewModel() {
        ...
    }

Now you can create your ViewModelProvider.Factory implementation:

    class MyViewModelFactory @Inject constructor(private val myViewModelProvider: Provider<MyViewModel>) : ViewModelProvider.Factory {

      @Suppress("UNCHECKED_CAST")
      override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return myViewModelProvider.get() as T
      }

    }

Dagger setup does not look too complicated:


    @Component(modules = [MyModule::class])
    interface MyComponent {
      fun inject(activity: MainActivity)
    }

    @Module
    abstract class MyModule {
      @Binds
      abstract fun bindsViewModelFactory(factory: MyViewModelFactory): ViewModelProvider.Factory
    }

Here's the activity class (might be fragment as well), where the actual injection takes place:


    class MainActivity : AppCompatActivity() {

      @Inject
      lateinit var factory: ViewModelProvider.Factory
      lateinit var viewModel: MyViewModel

      override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // retrieve the component from application class
        val component = MyApplication.getComponent()
        component.inject(this)

        viewModel = ViewModelProviders.of(this, factory).get(MyViewModel::class.java)
      }

    }

like image 7
azizbekian Avatar answered Oct 23 '22 13:10

azizbekian


No. You create a component where you are declaring (using) your viewModel. It is normally an activity/fragment. The viewModel has dependencies (mediaitemrepository), so you need a factory. Something like this:

    class MainViewModelFactory (
            val repository: IExerciseRepository): ViewModelProvider.Factory {

        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel?> create(p0: Class<T>?): T {
            return MainViewModel(repository) as T
        }
    }

Then the dagger part (activity module)

    @Provides
    @ActivityScope
    fun providesViewModelFactory(
            exerciseRepos: IExerciseRepository
    ) = MainViewModelFactory(exerciseRepos)

    @Provides
    @ActivityScope
    fun provideViewModel(
            viewModelFactory: MainViewModelFactory
    ): MainViewModel {
        return ViewModelProviders
                .of(act, viewModelFactory)
                .get(MainViewModel::class.java)
    }
like image 4
johnny_crq Avatar answered Oct 23 '22 13:10

johnny_crq