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 AndroidViewModel
faisait 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!
la source
AndroidViewModel
mais obtenant,Cannot create instance exception
vous pouvez vous référer à ma réponse stackoverflow.com/a/62626408/1055241Réponses:
Vous pouvez utiliser un
Application
contexte qui est fourni par leAndroidViewModel
, vous devez étendreAndroidViewModel
qui est simplement unViewModel
qui inclut uneApplication
référence.la source
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 }
la source
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.
la source
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.
la source
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
la source
getDrawableRes(@DrawableRes int id)
dans la classe ResourceProviderTL; 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!
la source
Comme d'autres l'ont mentionné,
AndroidViewModel
vous pouvez dériver pour obtenir l'application,Context
mais d'après ce que je comprends dans les commentaires, vous essayez de manipuler les@drawable
s depuis votre intérieur,ViewModel
ce qui va à l'encontre de l'objectif MVVM.En général, la nécessité d'avoir un
Context
dans votreViewModel
presque universellement suggère que vous devriez envisager de repenser la façon dont vous divisez la logique entre votreView
s etViewModels
.Au lieu de
ViewModel
ré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 leViewModel
. Disons que vous avez besoin de différents drawables pour être affichés dans une vue pour un état activé / désactivé - c'est celuiViewModel
qui doit contenir l'état (probablement booléen) mais c'estView
à 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
Enum
valeur enR.drawable.*
(par exemple, des combinaisons de cartes)Ou peut-être avez-vous besoin du
Context
pour un composant que vous utilisez dans votreViewModel
- puis, créez le composant en dehors deViewModel
et transmettez-le. Vous pouvez utiliser DI, ou des singletons, ou créer leContext
composant -dépendant juste avant d'initialiser leViewModel
inFragment
/Activity
.Pourquoi s'embêter:
Context
c'est une chose spécifique à Android, et en fonction de ceux deViewModel
s 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.la source
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
ViewModel
comme vous le feriez normalement, et donnez-lui le ApplicationContext via ViewModelProviders.Factory comme vous le feriez normalement.la source
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.la source
ViewModel
classe n'a pas lagetApplication
méthode.AndroidViewModel
faitVous 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
la source
J'avais du mal à obtenir
SharedPreferences
lors de l'utilisation duViewModel
cours, 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 maintenantPour 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; } }
la source
Je l'ai créé de cette façon:
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:
la source
Utilisez le modèle suivant:
la source