Comment obtenir du contexte dans Android MVVM ViewModel

90

J'essaie d'implémenter le modèle MVVM dans mon application Android. J'ai lu que ViewModels ne devrait contenir aucun code spécifique à Android (pour faciliter les tests), mais je dois utiliser le contexte pour diverses choses (obtenir des ressources à partir de xml, initialiser les préférences, etc.). Quelle est la meilleure façon de procéder? J'ai vu que cela AndroidViewModelfaisait référence au contexte de l'application, mais qui contient du code spécifique à Android, je ne suis donc pas sûr que cela devrait être dans le ViewModel. De plus, ceux-ci sont liés aux événements du cycle de vie de l'activité, mais j'utilise un poignard pour gérer la portée des composants, donc je ne sais pas comment cela l'affecterait. Je suis nouveau sur le modèle MVVM et Dagger, donc toute aide est appréciée!

Vincent Williams
la source
Juste au cas où quelqu'un essaie d'utiliser AndroidViewModelmais obtenant, Cannot create instance exceptionvous pouvez vous référer à ma réponse stackoverflow.com/a/62626408/1055241
gprathour
Vous ne devriez pas utiliser de contexte dans un ViewModel, créez plutôt un UseCase pour obtenir le contexte de cette façon
Ruben Caster

Réponses:

71

Vous pouvez utiliser un Applicationcontexte qui est fourni par le AndroidViewModel, vous devez étendre AndroidViewModelqui est simplement un ViewModelqui inclut une Applicationréférence.

Geai
la source
A travaillé comme un charme!
SPM
Quelqu'un pourrait-il montrer cela dans le code? Je suis à Java
Biswas Khayargoli
55

Pour le modèle de vue des composants d'architecture Android,

Ce n'est pas une bonne pratique de transmettre votre contexte d'activité au ViewModel de l'activité car il s'agit d'une fuite de mémoire.

Par conséquent, pour obtenir le contexte dans votre ViewModel, la classe ViewModel doit étendre la classe de modèle de vue Android . De cette façon, vous pouvez obtenir le contexte comme indiqué dans l'exemple de code ci-dessous.

class ActivityViewModel(application: Application) : AndroidViewModel(application) {

    private val context = getApplication<Application>().applicationContext

    //... ViewModel methods 

}
devDeejay
la source
2
Pourquoi ne pas utiliser directement le paramètre d'application et un ViewModel normal? Je ne vois aucun intérêt dans "getApplication <Application> ()". Cela ajoute simplement du passe-partout.
L'incroyable
50

Ce n'est pas que ViewModels ne devrait pas contenir de code spécifique à Android pour faciliter les tests, car c'est l'abstraction qui facilite les tests.

La raison pour laquelle les ViewModels ne doivent pas contenir d'instance de contexte ou quoi que ce soit comme des vues ou d'autres objets qui conservent un contexte est parce qu'il a un cycle de vie distinct de celui des activités et des fragments.

Ce que je veux dire par là, disons que vous effectuez un changement de rotation sur votre application. Cela provoque la destruction de votre activité et de votre fragment afin de se recréer. ViewModel est censé persister pendant cet état, il y a donc des risques de plantages et d'autres exceptions s'il contient toujours une vue ou un contexte pour l'activité détruite.

Quant à savoir comment faire ce que vous voulez faire, MVVM et ViewModel fonctionnent très bien avec le composant Databinding de JetPack. Pour la plupart des choses pour lesquelles vous stockez généralement une chaîne, un entier ou etc., vous pouvez utiliser la liaison de données pour que les vues l'affiche directement, évitant ainsi de stocker la valeur dans ViewModel.

Mais si vous ne voulez pas de liaison de données, vous pouvez toujours passer le contexte à l'intérieur du constructeur ou des méthodes pour accéder aux ressources. Ne tenez simplement pas une instance de ce contexte dans votre ViewModel.

Jackey
la source
1
J'avais cru comprendre que l'inclusion de code spécifique à Android nécessitait l'exécution de tests d'instrumentation beaucoup plus lents que les tests JUnit simples. J'utilise actuellement Databinding pour les méthodes de clic, mais je ne vois pas comment cela aiderait à obtenir des ressources à partir de xml ou pour les préférences. Je viens de réaliser que pour les préférences, j'aurais également besoin d'un contexte à l'intérieur de mon modèle. Ce que je fais actuellement, c'est que Dagger injecte le contexte de l'application (le module de contexte l'obtient à partir d'une méthode statique à l'intérieur de la classe d'application)
Vincent Williams
@VincentWilliams Oui, l'utilisation d'un ViewModel permet d'abstraire votre code de vos composants d'interface utilisateur, ce qui vous permet d'effectuer plus facilement des tests. Mais, ce que je dis, c'est que la principale raison pour ne pas inclure de contexte, de vue ou autre n'est pas due à des raisons de test, mais à cause du cycle de vie de ViewModel qui peut vous aider à éviter les plantages et autres erreurs. En ce qui concerne la liaison de données, cela peut vous aider avec les ressources, car la plupart du temps, vous devez accéder aux ressources dans le code parce que vous devez appliquer cette chaîne, cette couleur, dimen dans votre mise en page, ce que la liaison de données peut faire directement.
Jackey
Oh ok je vois ce que tu veux dire mais la liaison de données ne m'aidera pas dans ce cas car j'ai besoin d'accéder à des chaînes à utiliser dans le modèle (celles-ci pourraient être placées dans une classe de constantes au lieu de xml je suppose) et aussi pour initialiser SharedPreferences
Vincent Williams
3
si je veux basculer du texte dans une vue de texte basée sur un viewmodel de formulaire de valeur, la chaîne doit être localisée, donc j'ai besoin d'obtenir des ressources dans mon viewmodel, sans contexte comment vais-je accéder aux ressources?
Srishti Roy
3
@SrishtiRoy Si vous utilisez la liaison de données, il est facilement possible de basculer le texte d'un TextView en fonction de la valeur de votre modèle de vue. Il n'est pas nécessaire d'accéder à un contexte dans votre ViewModel car tout cela se produit dans les fichiers de disposition. Cependant, si vous devez utiliser un contexte dans votre ViewModel, vous devez envisager d'utiliser AndroidViewModel au lieu de ViewModel. AndroidViewModel contient le contexte d'application que vous pouvez appeler avec getApplication (), ce qui devrait satisfaire vos besoins en contexte si votre ViewModel nécessite un contexte.
Jackey
15

Réponse courte - Ne faites pas ça

Pourquoi ?

Cela va à l'encontre de tout l'objectif des modèles de vue

Presque tout ce que vous pouvez faire dans le modèle de vue peut être fait en activité / fragment en utilisant des instances LiveData et diverses autres approches recommandées.

humble_wolf
la source
21
Pourquoi alors la classe AndroidViewModel existe-t-elle même?
Alex Berdnikov le
1
@AlexBerdnikov Le but de MVVM est d'isoler la vue (Activité / Fragment) de ViewModel encore plus que MVP. Pour que ce soit plus facile à tester.
hushed_voice
3
@free_style Merci pour la clarification, mais la question demeure: si nous ne devons pas garder le contexte dans ViewModel, pourquoi la classe AndroidViewModel existe même? Son objectif est de fournir un contexte d'application, n'est-ce pas?
Alex Berdnikov
6
@AlexBerdnikov L'utilisation du contexte Activity dans le viewmodel peut provoquer des fuites de mémoire. Ainsi, en utilisant la classe AndroidViewModel, vous serez fourni par le contexte d'application qui ne causera (espérons-le) aucune fuite de mémoire. Donc, utiliser AndroidViewModel pourrait être mieux que de lui transmettre le contexte d'activité. Mais cela rendra les tests difficiles. Ceci est mon point de vue.
hushed_voice
1
Je ne peux pas accéder au fichier du dossier res / raw du référentiel?
Fugogugo
14

Ce que j'ai fini par faire au lieu d'avoir un contexte directement dans le ViewModel, j'ai créé des classes de fournisseur telles que ResourceProvider qui me donneraient les ressources dont j'ai besoin, et j'ai fait injecter ces classes de fournisseur dans mon ViewModel

Vincent Williams
la source
1
J'utilise ResourcesProvider avec Dagger dans AppModule. Est-ce que cette bonne approche pour obtenir le contexte de ResourcesProvider ou AndroidViewModel est préférable pour obtenir le contexte des ressources?
Usman Rana
@Vincent: Comment utiliser resourceProvider pour obtenir Drawable dans ViewModel?
HoangVu
@Vegeta Vous ajouteriez une méthode comme getDrawableRes(@DrawableRes int id)dans la classe ResourceProvider
Vincent Williams
1
Cela va à l'encontre de l'approche d'architecture propre qui stipule que les dépendances de cadre ne doivent pas traverser les limites de la logique de domaine (ViewModels).
IgorGanapolsky
1
Les VM @IgorGanapolsky ne sont pas exactement une logique de domaine. La logique de domaine comprend d'autres classes telles que les interacteurs et les référentiels pour n'en nommer que quelques-uns. Les VM entrent dans la catégorie "glue" car elles interagissent avec votre domaine, mais pas directement. Si vos VM font partie de votre domaine, vous devez reconsidérer la manière dont vous utilisez le modèle, car vous leur donnez trop de responsabilités.
mradzinski
8

TL; DR: Injectez le contexte de l'application via Dagger dans vos ViewModels et utilisez-le pour charger les ressources. Si vous avez besoin de charger des images, passez l'instance View via des arguments des méthodes de liaison de données et utilisez ce contexte View.

Le MVVM est une bonne architecture et c'est certainement l'avenir du développement Android, mais il y a quelques choses qui sont encore vertes. Prenons par exemple la communication de couche dans une architecture MVVM, j'ai vu différents développeurs (des développeurs très connus) utiliser LiveData pour communiquer les différentes couches de différentes manières. Certains d'entre eux utilisent LiveData pour communiquer le ViewModel avec l'interface utilisateur, mais ils utilisent ensuite des interfaces de rappel pour communiquer avec les référentiels, ou ils ont des Interactors / UseCases et ils utilisent LiveData pour communiquer avec eux. Le point ici, c'est que tout n'est pas encore défini à 100% .

Cela étant dit, mon approche avec votre problème spécifique est d'avoir le contexte d'une application disponible via DI à utiliser dans mes ViewModels pour obtenir des éléments tels que String à partir de mes strings.xml

Si je traite le chargement d'image, j'essaie de passer par les objets View à partir des méthodes de l'adaptateur Databinding et d'utiliser le contexte de View pour charger les images. Pourquoi? car certaines technologies (par exemple Glide) peuvent rencontrer des problèmes si vous utilisez le contexte de l'application pour charger des images.

J'espère que cela aide!

4gus71n
la source
5
TL; DR devrait être au sommet
Jacques Koorts
1
Merci pour votre réponse. Cependant, pourquoi utiliseriez-vous dagger pour injecter le contexte si vous pouviez étendre votre viewmodel à partir de androidviewmodel et utiliser le contexte intégré que la classe elle-même fournit? Surtout compte tenu de la quantité ridicule de code standard pour faire fonctionner Dagger et MVVM ensemble, l'autre solution semble beaucoup plus claire à mon avis. Que pensez-vous de ceci?
Josip Domazet
7

Comme d'autres l'ont mentionné, AndroidViewModelvous pouvez dériver pour obtenir l'application, Contextmais d'après ce que je comprends dans les commentaires, vous essayez de manipuler les @drawables depuis votre intérieur, ViewModelce qui va à l'encontre de l'objectif MVVM.

En général, la nécessité d'avoir un Contextdans votre ViewModelpresque universellement suggère que vous devriez envisager de repenser la façon dont vous divisez la logique entre votre Views et ViewModels.

Au lieu de ViewModelrésoudre les drawables et de les alimenter dans l'activité / le fragment, envisagez de demander au fragment / activité de jongler avec les drawables en fonction des données possédées par le ViewModel. Disons que vous avez besoin de différents drawables pour être affichés dans une vue pour un état activé / désactivé - c'est celui ViewModelqui doit contenir l'état (probablement booléen) mais c'est Viewà lui de sélectionner le dessinable en conséquence.

Cela peut être fait assez facilement avec DataBinding :

<ImageView
...
app:src="@{viewModel.isOn ? @drawable/switch_on : @drawable/switch_off}"
/>

Si vous avez plus d'états et de dessinables, pour éviter une logique lourde dans le fichier de mise en page, vous pouvez écrire un BindingAdapter personnalisé qui traduit, par exemple, une Enumvaleur en R.drawable.*(par exemple, des combinaisons de cartes)

Ou peut-être avez-vous besoin du Contextpour un composant que vous utilisez dans votre ViewModel- puis, créez le composant en dehors de ViewModelet transmettez-le. Vous pouvez utiliser DI, ou des singletons, ou créer le Contextcomposant -dépendant juste avant d'initialiser le ViewModelin Fragment/ Activity.

Pourquoi s'embêter: Contextc'est une chose spécifique à Android, et en fonction de ceux de ViewModels est une mauvaise pratique: ils font obstacle aux tests unitaires. D'autre part, vos propres interfaces composant / service sont entièrement sous votre contrôle, vous pouvez donc facilement les simuler pour les tester.

Ivan Bartsov
la source
5

a une référence au contexte de l'application, mais qui contient du code spécifique à Android

Bonne nouvelle, vous pouvez utiliser Mockito.mock(Context.class)et faire en sorte que le contexte renvoie ce que vous voulez dans les tests!

Donc, utilisez simplement un ViewModelcomme vous le feriez normalement, et donnez-lui le ApplicationContext via ViewModelProviders.Factory comme vous le feriez normalement.

EpicPandaForce
la source
3

vous pouvez accéder au contexte de l'application à partir getApplication().getApplicationContext()de ViewModel. C'est ce dont vous avez besoin pour accéder aux ressources, aux préférences, etc.

Alessandro Crugnola
la source
Je suppose que pour restreindre ma question. Est-il mauvais d'avoir une référence de contexte dans le viewmodel (cela n'affecte-t-il pas les tests?) Et l'utilisation de la classe AndroidViewModel affecterait-elle Dagger d'une manière ou d'une autre? N'est-ce pas lié au cycle de vie de l'activité? J'utilise Dagger pour contrôler le cycle de vie des composants
Vincent Williams
14
La ViewModelclasse n'a pas la getApplicationméthode.
béroal
4
Non mais AndroidViewModel fait
4Oh4
1
Mais vous devez passer l'instance Application dans son constructeur, c'est la même chose que d'accéder à l'instance Application à partir de celle-ci
John Sardinha
2
Le contexte d'application ne pose pas de problème. Vous ne voulez pas avoir un contexte d'activité / fragment parce que vous êtes embarrassé si le fragment / activité est détruit et que le modèle de vue a toujours une référence au contexte désormais inexistant. Mais le contexte APPLICATION ne sera jamais détruit, mais la VM y fait toujours référence. Droite? Pouvez-vous imaginer un scénario dans lequel votre application se ferme mais pas le Viewmodel? :)
user1713450
3

Vous ne devez pas utiliser d'objets liés à Android dans votre ViewModel car le motif de l'utilisation d'un ViewModel est de séparer le code java et le code Android afin que vous puissiez tester votre logique métier séparément et vous disposerez d'une couche séparée de composants Android et de votre logique métier. et les données, vous ne devriez pas avoir de contexte dans votre ViewModel car cela peut entraîner des plantages

Rohit Sharma
la source
2
C'est une observation juste, mais certaines des bibliothèques de backend nécessitent toujours des contextes d'application, tels que MediaStore. La réponse de 4gus71n ci-dessous explique comment faire des compromis.
Bryan
1
Oui, vous pouvez utiliser le contexte de l'application mais pas le contexte des activités, car le contexte de l'application vit tout au long du cycle de vie de l'application, mais pas le contexte d'activité, car le fait de transmettre le contexte d'activité à un processus asynchrone peut entraîner des fuites de mémoire. Contexte.Mais vous devez toujours veiller à ne pas transmettre de contexte à un processus asynchrone, même s'il s'agit d'un contexte d'applications.
Rohit Sharma
2

J'avais du mal à obtenir SharedPreferenceslors de l'utilisation du ViewModelcours, j'ai donc suivi les conseils des réponses ci-dessus et j'ai fait ce qui suit en utilisantAndroidViewModel . Tout a l'air bien maintenant

Pour le AndroidViewModel

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;

import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.preference.PreferenceManager;

public class HomeViewModel extends AndroidViewModel {

    private MutableLiveData<String> some_string;

    public HomeViewModel(Application application) {
        super(application);
        some_string = new MutableLiveData<>();
        Context context = getApplication().getApplicationContext();
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        some_string.setValue("<your value here>"));
    }

}

Et dans le Fragment

import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;


public class HomeFragment extends Fragment {


    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {
        final View root = inflater.inflate(R.layout.fragment_home, container, false);
        HomeViewModel homeViewModel = ViewModelProviders.of(this).get(HomeViewModel.class);
        homeViewModel.getAddress().observe(getViewLifecycleOwner(), new Observer<String>() {
            @Override
            public void onChanged(@Nullable String address) {


            }
        });
        return root;
    }
}
Davejoem
la source
0

Je l'ai créé de cette façon:

@Module
public class ContextModule {

    @Singleton
    @Provides
    @Named("AppContext")
    public Context provideContext(Application application) {
        return application.getApplicationContext();
    }
}

Et puis je viens d'ajouter dans AppComponent le ContextModule.class:

@Component(
       modules = {
                ...
               ContextModule.class
       }
)
public interface AppComponent extends AndroidInjector<BaseApplication> {
.....
}

Et puis j'ai injecté le contexte dans mon ViewModel:

@Inject
@Named("AppContext")
Context context;
Loopidio
la source
0

Utilisez le modèle suivant:

class NameViewModel(
val variable:Class,application: Application):AndroidViewModel(application){
   body...
}
EhsanFallahi
la source