web-dev-qa-db-fra.com

Bibliothèque de cycle de vie Android ViewModel using dagger 2

J'ai une classe ViewModel similaire à celle définie dans la section Connecting ViewModel and repository de Guide d'architecture . Lorsque je lance mon application, je reçois une exception d'exécution. Est-ce que quelqu'un sait comment contourner cela? Ne devrais-je pas injecter le ViewModel? Y a-t-il un moyen de dire à la ViewModelProvider d'utiliser Dagger pour créer le modèle?

public class DispatchActivityModel extends ViewModel {

    private final API api;

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

Causée par: Java.lang.InstantiationException: Java.lang.Class n'a pas de constructeur à argument zéro à Java.lang.Class.newInstance (Méthode native) sur Android.Arch.lifecycle.ViewModelProvider $ NewInstanceFactory.create (ViewModelProvider.Java:143) sur Android.Arch.lifecycle.ViewModelProviders $ DefaultFactory.create (ViewModelProviders.Java:143) sur Android.Arch.lifecycle.ViewModelProvider.get (ViewModelProvider.Java:128) sur Android.Arch.lifecycle.ViewModelProvider.get (ViewModelProvider.Java:96) à l'adresse com.example.base.BaseActivity.onCreate (BaseActivity.Java:65) à l'adresse com.example.dispatch.DispatchActivity.onCreate (DispatchActivity.Java:53) sur Android.app.Activity.performCreate (Activity.Java:6682) sur Android.app.Instrumentation.callActivityOnCreate (Instrumentation.Java:1118) sur Android.app.ActivityThread.performLaunchActivity (ActivityThread.Java:2619) sur Android.app.ActivityThread.handleLaunchActivity (ActivityThread.Java:2727). sur Android.app.ActivityThread.-wrap12 (ActivityThread.Java) sur Android.app.ActivityThread $ H.handleMessage (ActivityThread.Java:1478) sur Android.os.Handler.dispatchMessage (Handler.Java:102) sur Android.os.Looper.loop (Looper.Java:154) sur Android.app.ActivityThread.main (ActivityThread.Java:6121)

45
TheHebrewHammer

Vous devez implémenter votre propre ViewModelProvider.Factory. Un exemple d'application créé par Google montre comment connecter Dagger 2 à ViewModels. LIEN . Vous avez besoin de ces 5 choses:

Dans ViewModel:

@Inject
public UserViewModel(UserRepository userRepository, RepoRepository repoRepository) {

Définir l'annotation:

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@MapKey
@interface ViewModelKey {
    Class<? extends ViewModel> value();
}

Dans ViewModelModule:

@Module
abstract class ViewModelModule {
    @Binds
    @IntoMap
    @ViewModelKey(UserViewModel.class)
    abstract ViewModel bindUserViewModel(UserViewModel userViewModel);

En fragment:

@Inject
ViewModelProvider.Factory viewModelFactory;

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            userViewModel = ViewModelProviders.of(this, viewModelFactory).get(UserViewModel.class);

Usine:

@Singleton
public class GithubViewModelFactory implements ViewModelProvider.Factory {
    private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;

    @Inject
    public GithubViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
        this.creators = creators;
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        Provider<? extends ViewModel> creator = creators.get(modelClass);
        if (creator == null) {
            for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
                if (modelClass.isAssignableFrom(entry.getKey())) {
                    creator = entry.getValue();
                    break;
                }
            }
        }
        if (creator == null) {
            throw new IllegalArgumentException("unknown model class " + modelClass);
        }
        try {
            return (T) creator.get();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
74
Robert Wysocki

Aujourd'hui, j'ai appris à éviter d'écrire des usines pour mes classes ViewModel:

class ViewModelFactory<T : ViewModel> @Inject constructor(
    private val viewModel: Lazy<T>
) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T = viewModel.get() as T
}

EDIT: Comme l'a souligné @Calin dans les commentaires, nous utilisons la variable Lazy de Dagger dans l'extrait de code ci-dessus, pas celle de Kotlin. 

Plutôt que d’injecter la ViewModel, vous pouvez injecter une ViewModelFactory générique dans vos activités et fragments et obtenir une instance de toute ViewModel:

class MyActivity : Activity() {

    @Inject
    internal lateinit var viewModelFactory: ViewModelFactory<MyViewModel>
    private lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
        this.viewModel = ViewModelProviders.of(this, viewModelFactory)
            .get(MyViewModel::class.Java)
        ...
    }

    ...
}

J'ai utilisé AndroidInjection.inject(this) comme avec la bibliothèque dagger-Android, mais vous pouvez injecter votre activité ou fragmenter comme vous le souhaitez. Tout ce qui reste à faire est de vous assurer que vous fournissez votre ViewModel à partir d'un module:

@Module
object MyModule {
    @JvmStatic
    @Provides
    fun myViewModel(someDependency: SomeDependency) = MyViewModel(someDependency)
} 

Ou en appliquant l'annotation @Inject à son constructeur:

class MyViewModel @Inject constructor(
    someDependency: SomeDependency
) : ViewModel() {
    ...
}
19
argenkiwi

Je crois qu'il existe une deuxième option si vous ne souhaitez pas utiliser l'usine mentionnée dans la réponse de Robert. Ce n'est pas forcément la meilleure solution mais il est toujours bon de connaître les options.

Vous pouvez laisser votre viewModel avec le constructeur par défaut et injecter vos dépendances exactement comme vous le faites dans le cas d'activités ou d'autres éléments créés par system . Exemple:

ViewModel:

public class ExampleViewModel extends ViewModel {

@Inject
ExampleDependency exampleDependency;

public ExampleViewModel() {
    DaggerExampleComponent.builder().build().inject(this);
    }
}

Composant:

@Component(modules = ExampleModule.class)
public interface ExampleComponent {

void inject(ExampleViewModel exampleViewModel);

}

Module:

@Module
public abstract class ExampleModule {

@Binds
public abstract ExampleDependency bindExampleDependency(ExampleDependencyDefaultImplementation exampleDependencyDefaultImplementation);

}

À la vôtre, Piotr

4

Ce qui n’est peut-être pas évident dans la question, c’est que ViewModel ne peut pas être injecté de cette façon, car l’usine par défaut ViewModelProvider que vous obtenez du 

ViewModelProvider.of(LifecycleOwner lo) 

une méthode avec uniquement le paramètre LifecycleOwner ne peut instancier qu'un ViewModel doté d'un constructeur par défaut sans argument.

Vous avez un param: 'api' dans votre constructeur: 

public DispatchActivityModel(API api) {

Pour ce faire, vous devez créer une fabrique afin de lui indiquer comment se créer. L'exemple de code fourni par Google vous donne la configuration de la dague et le code d'usine, comme indiqué dans la réponse acceptée. 

DI a été créé pour éviter l'utilisation de l'opérateur new () sur les dépendances, car si les implémentations changent, chaque référence devra également changer. L'implémentation ViewModel utilise judicieusement un modèle d'usine statique déjà avec ViewProvider.of (). Get (), ce qui rend son injection inutile dans le cas du constructeur sans argument. Donc, dans le cas où vous n'avez pas besoin d'écrire l'usine, vous n'avez évidemment pas besoin d'injecter une usine.

4
Droid Teahouse

J'aimerais proposer une troisième option à quiconque trébuche sur cette question. La bibliothèque Dagger ViewModel vous permettra d’injecter Dagger2 comme ViewModels en spécifiant éventuellement la portée de ViewModel. 

Il supprime une grande partie du passe-partout et permet également aux ViewModels d'être injectés de manière déclarative à l'aide d'une annotation:

@InjectViewModel(useActivityScope = true)
public MyFragmentViewModel viewModel;

Il nécessite également une petite quantité de code pour configurer un module à partir duquel les ViewModels injectés entièrement dépendants peuvent être générés. Il suffit ensuite d'appeler:

void injectFragment(Fragment fragment, ViewModelFactory factory) {
    ViewModelInejectors.inject(frag, viewModelFactory);
}

Sur la classe ViewModelInjectors qui est générée.

CLAUSE DE NON-RESPONSABILITÉ: c’est ma bibliothèque, mais je pense que c’est également utile pour l’auteur de cette question et pour tous ceux qui souhaitent réaliser la même chose.

3
onepointsixtwo

La fabrique ViewModel par défaut utilisée pour obtenir une instance de votre DispatchActivityModel dans votre vue construit les ViewModels à l'aide de constructeurs vides supposés.

Vous pouvez écrire votre ViewModel.Factory personnalisé pour le contourner, mais vous devrez toujours vous occuper de compléter le graphique de dépendance vous-même si vous souhaitez fournir votre classe API.

J'ai écrit une petite bibliothèque qui devrait rendre la résolution de ce problème plus simple et plus simple, sans multibindings ni embase standard, tout en donnant la possibilité de paramétrer davantage la ViewModel au moment de l'exécution: https://github.com/ radutopor/ViewModelFactory

@ViewModelFactory
public class DispatchActivityModel extends ViewModel {

    private final API api;

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

Dans la vue:

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

Comme je l'ai mentionné, vous pouvez également ajouter facilement des paramètres d'exécution à vos instances ViewModel:

@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)
    }
}
0
Radu Topor