web-dev-qa-db-fra.com

Injecter ViewModel à l'aide de Dagger 2 + Kotlin + ViewModel

class SlideshowViewModel : ViewModel() {

@Inject lateinit var mediaItemRepository : MediaItemRepository

fun init() {
    What goes here?
}

J'essaie donc d'apprendre Dagger2 afin de rendre mes applications plus testables. Le problème, c'est que j'ai déjà intégré Kotlin et que je travaille sur les composants Android Architectural. Je comprends que l’injection par le constructeur est préférable, mais ce n’est pas possible avec ViewModel. Au lieu de cela, je peux utiliser lateinit pour injecter mais je ne sais pas comment l'injecter.

Dois-je créer une Component pour SlideshowViewModel, puis l'injecter? Ou est-ce que j'utilise le composant Application?

grade:

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'
}

Composant d'application

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

    void injectBaseApplication(BaseApplication baseApplication);
}

Application de base

    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;
    }
13
easycheese

Vous pouvez activer l'injection de constructeur pour vos ViewModels. Vous pouvez consulter exemples Google pour savoir comment procéder en Java. (Mise à jour: on dirait qu'ils ont converti le projet en Kotlin afin que cette URL ne fonctionne plus)

Voici comment faire la même chose à Kotlin:

Ajouter une annotation ViewModelKey:

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>)

Ajouter 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)
        }
    }
}

Ajouter 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
}

Enregistrez votre ViewModelModule dans votre composant

Injectez ViewModelProvider.Factory dans votre activité:

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

Passez votre modelFactory à chaque méthode ViewModelProviders.of:

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

Voici l'exemple de validation contenant toutes les modifications requises: Prise en charge de l'injection du constructeur pour les modèles de vue

5
Igor Bubelov

Reportez-vous à un repo que j'ai créé quand j'apprenais dagger + kotlin

Vous avez essentiellement besoin d'une instance ViewModelFactory pour la couche d'interface utilisateur, que vous utilisez pour créer un modèle de vue.

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


    @SuppressWarnings("Unchecked")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        var creator = creators[modelClass]

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

        if (creator == null) throw IllegalArgumentException("Unknown model class" + modelClass)

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

Votre ViewModelModule devrait ressembler à ceci (c'est ici que vous stockez tous les modèles de vues). 

@Module
abstract class ViewModelModule {
    @AppScope
    @Binds
    @IntoMap
    @ViewModelKey(YourViewModel::class)
    abstract fun bindsYourViewModel(yourViewModel: YourViewModel): ViewModel

    // Factory
    @AppScope
    @Binds abstract fun bindViewModelFactory(vmFactory: ViewModelFactory): ViewModelProvider.Factory
}

Créez ensuite une clé de carte de poignard

@Documented
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

Ensuite, sur votre couche d'interface utilisateur, injectez la fabrique et instanciez votre modèle de vue à l'aide de ViewModelProviders

class YourActivity : BaseActivity() {
    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    lateinit var yourViewModel: YourViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        ...
        (application as App).component.inject(this)
    }

    override fun onStart() {
        super.onStart()
        yourViewModel = ViewModelProviders.of(this, viewModelFactory).get(YourViewModel::class.Java)

        // you can now use your viewmodels properties and methods
        yourViewModel.methodName() 
        yourViewModel.list.observe(this, { ... })

    }
4
Vaughn Armada

En supposant que vous ayez une classe Repository pouvant être injectée par Dagger et une classe MyViewModel ayant une dépendance sur Repository définie comme telle:


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

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

Vous pouvez maintenant créer votre implémentation ViewModelProvider.Factory:

    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
      }

    }

L'installation de la dague n'a pas l'air trop compliquée:


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

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

Voici la classe d'activité (peut-être aussi un fragment), où l'injection réelle a lieu:


    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)
      }

    }
2
azizbekian

Non. Vous créez un composant dans lequel vous déclarez (utilisez) votre viewModel. C'est normalement une activité/un fragment. Le viewModel a des dépendances (mediaitemrepository), vous avez donc besoin d'une usine. Quelque chose comme ça:

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

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

Puis la partie poignard (module d'activité)

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

    @Provides
    @ActivityScope
    fun provideViewModel(
            viewModelFactory: MainViewModelFactory
    ): MainViewModel {
        return ViewModelProviders
                .of(act, viewModelFactory)
                .get(MainViewModel::class.Java)
    }
2
johnny_crq

Essayez avec le code ci-dessous:

@Provides
@Singleton
fun provideRepository(): Repository {
    return Repository(DataSource())
}
0
Harsh Agrawal

J'ai écrit une bibliothèque qui devrait rendre cela plus simple et plus clair, sans multibindings ni embase standard, tout en donnant la possibilité de paramétrer davantage la ViewModel à l'exécution: https://github.com/radutopor/ViewModelFactory

@ViewModelFactory
class UserViewModel(@Provided repository: Repository, userId: Int) : ViewModel() {

    val greeting = MutableLiveData<String>()

    init {
        val user = repository.getUser(userId)
        greeting.value = "Hello, $user.name"
    }    
}

Dans la vue:

class UserActivity : AppCompatActivity() {
    @Inject
    lateinit var userViewModelFactory2: UserViewModelFactory2

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)
        appComponent.inject(this)

        val userId = intent.getIntExtra("USER_ID", -1)
        val viewModel = ViewModelProviders.of(this, userViewModelFactory2.create(userId))
            .get(UserViewModel::class.Java)

        viewModel.greeting.observe(this, Observer { greetingText ->
            greetingTextView.text = greetingText
        })
    }
}
0
Radu Topor