So erhalten Sie Kontext in Android MVVM ViewModel

90

Ich versuche, MVVM-Muster in meiner Android-App zu implementieren. Ich habe gelesen, dass ViewModels keinen androidspezifischen Code enthalten sollten (um das Testen zu vereinfachen), ich muss jedoch den Kontext für verschiedene Dinge verwenden (Ressourcen aus XML abrufen, Einstellungen initialisieren usw.). Was ist der beste Weg, dies zu tun? Ich habe gesehen, dass dies AndroidViewModeleinen Verweis auf den Anwendungskontext enthält, der jedoch androidspezifischen Code enthält, sodass ich nicht sicher bin, ob dies im ViewModel enthalten sein sollte. Auch diese hängen mit den Ereignissen im Aktivitätslebenszyklus zusammen, aber ich verwende Dolch, um den Umfang der Komponenten zu verwalten, sodass ich nicht sicher bin, wie sich dies darauf auswirken würde. Ich bin neu im MVVM-Muster und im Dolch, daher ist jede Hilfe willkommen!

Vincent Williams
quelle
Nur für den Fall, dass jemand versucht, es zu benutzen, AndroidViewModelaber es Cannot create instance exceptionbekommt, können Sie sich auf meine Antwort stackoverflow.com/a/62626408/1055241
gprathour
Sie sollten Context nicht in einem ViewModel verwenden, sondern stattdessen einen UseCase erstellen, um den Kontext auf diese Weise abzurufen
Ruben Caster

Antworten:

71

Sie können einen verwenden ApplicationKontext, der durch die vorgesehen ist AndroidViewModel, sollten Sie erweitern AndroidViewModeldie einfach eine ist , ViewModeldie eine beinhaltet ApplicationReferenz.

Jay
quelle
Lief wie am Schnürchen!
SPM
Könnte jemand dies im Code zeigen? Ich bin in Java
Biswas Khayargoli
55

Für Android-Architekturkomponenten Modell anzeigen,

Es ist keine gute Vorgehensweise, Ihren Aktivitätskontext an das ViewModel der Aktivität zu übergeben, da es sich um einen Speicherverlust handelt.

Um den Kontext in Ihrem ViewModel zu erhalten, sollte die ViewModel-Klasse daher die Android View Model- Klasse erweitern. Auf diese Weise können Sie den Kontext wie im folgenden Beispielcode gezeigt abrufen.

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

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

    //... ViewModel methods 

}
devDeejay
quelle
2
Warum nicht direkt den Anwendungsparameter und ein normales ViewModel verwenden? Ich sehe keinen Sinn in "getApplication <Application> ()". Es fügt nur Boilerplate hinzu.
Der unglaubliche
50

Es ist nicht so, dass ViewModels keinen Android-spezifischen Code enthalten sollten, um das Testen zu vereinfachen, da es die Abstraktion ist, die das Testen erleichtert.

Der Grund, warum ViewModels keine Instanz von Context oder Ähnlichem wie Views oder anderen Objekten enthalten sollten, die an einem Context festhalten, liegt darin, dass es einen anderen Lebenszyklus als Aktivitäten und Fragmente hat.

Damit meine ich, dass Sie eine Rotationsänderung an Ihrer App vornehmen. Dies führt dazu, dass sich Ihre Aktivität und Ihr Fragment selbst zerstören und sich selbst neu erstellen. ViewModel soll in diesem Zustand bestehen bleiben, sodass Abstürze und andere Ausnahmen auftreten können, wenn noch eine Ansicht oder ein Kontext für die zerstörte Aktivität vorhanden ist.

MVVM und ViewModel funktionieren sehr gut mit der Datenbindungskomponente von JetPack. Für die meisten Dinge, für die Sie normalerweise einen String, ein Int usw. speichern, können Sie die Datenbindung verwenden, damit die Ansichten sie direkt anzeigen, sodass der Wert nicht in ViewModel gespeichert werden muss.

Wenn Sie jedoch keine Datenbindung wünschen, können Sie den Kontext dennoch im Konstruktor oder in den Methoden übergeben, um auf die Ressourcen zuzugreifen. Halten Sie nur keine Instanz dieses Kontexts in Ihrem ViewModel.

Jackey
quelle
1
Nach meinem Verständnis mussten für die Aufnahme von androidspezifischem Code Instrumentierungstests ausgeführt werden, die viel langsamer sind als einfache JUnit-Tests. Ich verwende derzeit die Datenbindung für Klickmethoden, sehe jedoch nicht, wie dies beim Abrufen von Ressourcen aus XML oder für Einstellungen hilfreich sein könnte. Ich habe gerade festgestellt, dass ich für Präferenzen auch einen Kontext innerhalb meines Modells benötigen würde. Was ich gerade mache, ist, dass Dagger den Anwendungskontext einfügt (das Kontextmodul erhält ihn von einer statischen Methode innerhalb der Anwendungsklasse)
Vincent Williams
@VincentWilliams Ja, die Verwendung eines ViewModel hilft dabei, Ihren Code von Ihren UI-Komponenten zu abstrahieren, was Ihnen das Durchführen von Tests erleichtert. Ich sage jedoch, dass der Hauptgrund für das Nichteinschließen von Kontext, Ansichten oder Ähnlichem nicht in Testgründen liegt, sondern im Lebenszyklus des ViewModel, mit dem Sie Abstürze und andere Fehler vermeiden können. Die Datenbindung kann Ihnen bei den Ressourcen helfen, da die meiste Zeit, die Sie für den Zugriff auf die Ressourcen im Code benötigen, darauf zurückzuführen ist, dass Sie diese Zeichenfolge, Farbe und Dimension in Ihr Layout anwenden müssen, was die Datenbindung direkt tun kann.
Jackey
Oh ok, ich verstehe, was Sie meinen, aber die Datenbindung hilft mir in diesem Fall nicht weiter, da ich auf Zeichenfolgen zugreifen muss, um sie im Modell zu verwenden (diese könnten vermutlich in eine Konstantenklasse anstelle von XML eingefügt werden) und auch um SharedPreferences zu initialisieren
Vincent Williams
3
Wenn ich Text in einer Textansicht basierend auf einem Wertformular-Ansichtsmodell umschalten möchte, muss die Zeichenfolge lokalisiert werden, damit ich Ressourcen in meinem Ansichtsmodell ohne Kontext abrufen kann. Wie kann ich auf die Ressourcen zugreifen?
Srishti Roy
3
@SrishtiRoy Wenn Sie die Datenbindung verwenden, können Sie den Text einer Textansicht leicht basierend auf dem Wert Ihres Ansichtsmodells umschalten. In Ihrem ViewModel ist kein Zugriff auf einen Kontext erforderlich, da dies alles in den Layoutdateien geschieht. Wenn Sie jedoch einen Kontext in Ihrem ViewModel verwenden müssen, sollten Sie das AndroidViewModel anstelle von ViewModel verwenden. AndroidViewModel enthält den Anwendungskontext, den Sie mit getApplication () aufrufen können, sodass Ihre Kontextanforderungen erfüllt werden sollten, wenn Ihr ViewModel einen Kontext benötigt.
Jackey
15

Kurze Antwort - Tu das nicht

Warum ?

Es macht den gesamten Zweck von Ansichtsmodellen zunichte

Fast alles, was Sie im Ansichtsmodell tun können, kann in Aktivität / Fragment mithilfe von LiveData-Instanzen und verschiedenen anderen empfohlenen Ansätzen ausgeführt werden.

bescheidener Wolf
quelle
21
Warum gibt es dann überhaupt eine AndroidViewModel-Klasse?
Alex Berdnikov
1
@AlexBerdnikov Der Zweck von MVVM besteht darin, die Ansicht (Aktivität / Fragment) noch stärker als MVP von ViewModel zu isolieren. Damit es einfacher zu testen ist.
hushed_voice
3
@free_style Vielen Dank für die Klarstellung, aber die Frage bleibt: Wenn wir den Kontext in ViewModel nicht beibehalten dürfen, warum existiert die AndroidViewModel-Klasse überhaupt? Der gesamte Zweck besteht darin, den Anwendungskontext bereitzustellen, nicht wahr?
Alex Berdnikov
6
@AlexBerdnikov Die Verwendung des Aktivitätskontexts im Ansichtsmodell kann zu Speicherverlusten führen. Wenn Sie also die AndroidViewModel-Klasse verwenden, werden Sie vom Anwendungskontext bereitgestellt, der (hoffentlich) keinen Speicherverlust verursacht. Die Verwendung von AndroidViewModel ist daher möglicherweise besser als die Übergabe des Aktivitätskontexts. Trotzdem wird das Testen schwierig. Das ist meine Einstellung dazu.
hushed_voice
1
Ich kann nicht auf die Datei aus dem Ordner res / raw aus dem Repository zugreifen.
Fugogugo
14

Was ich letztendlich getan habe, anstatt einen Kontext direkt im ViewModel zu haben, habe ich Anbieterklassen wie ResourceProvider erstellt, die mir die benötigten Ressourcen zur Verfügung stellen, und diese Anbieterklassen wurden in mein ViewModel eingefügt

Vincent Williams
quelle
1
Ich verwende ResourcesProvider mit Dolch in AppModule. Ist dieser gute Ansatz, um den Kontext für ResourcesProvider oder AndroidViewModel abzurufen, besser, um den Kontext für Ressourcen abzurufen?
Usman Rana
@ Vincent: Wie verwende ich resourceProvider, um Drawable in ViewModel zu erhalten?
HoangVu
@ Vegeta Sie würden eine Methode wie getDrawableRes(@DrawableRes int id)innerhalb der ResourceProvider-Klasse hinzufügen
Vincent Williams
1
Dies widerspricht dem Clean Architecture-Ansatz, der besagt, dass Framework-Abhängigkeiten keine Grenzen in der Domänenlogik (ViewModels) überschreiten dürfen.
IgorGanapolsky
1
@IgorGanapolsky VMs sind nicht gerade Domänenlogik. Domänenlogik sind andere Klassen wie Interaktoren und Repositorys, um nur einige zu nennen. VMs fallen in die Kategorie "Kleber", da sie zwar mit Ihrer Domain interagieren, jedoch nicht direkt. Wenn Ihre VMs Teil Ihrer Domain sind, sollten Sie die Verwendung des Musters überdenken, da Sie ihnen zu viel Verantwortung übertragen.
mradzinski
8

TL; DR: Fügen Sie den Kontext der Anwendung über Dagger in Ihre ViewModels ein und verwenden Sie ihn zum Laden der Ressourcen. Wenn Sie Bilder laden müssen, übergeben Sie die View-Instanz über Argumente aus den Datenbindungsmethoden und verwenden Sie diesen View-Kontext.

Die MVVM ist eine gute Architektur und es ist definitiv die Zukunft der Android-Entwicklung, aber es gibt ein paar Dinge, die noch grün sind. Nehmen wir zum Beispiel die Layer-Kommunikation in einer MVVM-Architektur. Ich habe gesehen, dass verschiedene Entwickler (sehr bekannte Entwickler) LiveData verwenden, um die verschiedenen Layer auf unterschiedliche Weise zu kommunizieren. Einige von ihnen verwenden LiveData, um das ViewModel mit der Benutzeroberfläche zu kommunizieren, aber dann verwenden sie Rückrufschnittstellen, um mit den Repositorys zu kommunizieren, oder sie haben Interactors / UseCases und sie verwenden LiveData, um mit ihnen zu kommunizieren. Punkt hier ist, dass noch nicht alles zu 100% definiert ist .

Abgesehen davon besteht mein Ansatz bei Ihrem spezifischen Problem darin, den Kontext einer Anwendung über DI verfügbar zu machen, um ihn in meinen ViewModels zu verwenden, um Dinge wie String aus meiner Datei strings.xml abzurufen

Wenn ich mich mit dem Laden von Bildern beschäftige, versuche ich, die View-Objekte aus den Methoden des Datenbindungsadapters zu durchlaufen und den Kontext der Ansicht zum Laden der Bilder zu verwenden. Warum? da bei einigen Technologien (z. B. Glide) Probleme auftreten können, wenn Sie den Kontext der Anwendung zum Laden von Bildern verwenden.

Ich hoffe es hilft!

4gus71n
quelle
5
TL; DR sollte an der Spitze sein
Jacques Koorts
1
Vielen Dank für Ihre Antwort. Warum sollten Sie jedoch Dolch verwenden, um den Kontext einzufügen, wenn Sie Ihr Ansichtsmodell von androidviewmodel erweitern und den integrierten Kontext verwenden könnten, den die Klasse selbst bereitstellt? Besonders angesichts der lächerlichen Menge an Boilerplate-Code, mit der Dolch und MVVM zusammenarbeiten, scheint die andere Lösung imo viel klarer zu sein. Was denkst du darüber?
Josip Domazet
7

Wie andere bereits erwähnt haben, gibt es etwas, von AndroidViewModeldem Sie ableiten können, um die App zu erhalten, Contextaber von dem, was ich in den Kommentaren erfahre, versuchen Sie, @drawables aus Ihrem Inneren heraus zu manipulierenViewModel was den Zweck von MVVM zunichte macht.

Im Allgemeinen legt die Notwendigkeit, ein Contextin Ihrem ViewModelfast universellen zu haben, nahe, dass Sie überlegen sollten, wie Sie die Logik zwischen Ihrem Views und aufteilen ViewModels.

Anstatt ViewModelDrawables aufzulösen und sie der Aktivität / dem Fragment zuzuführen, sollten Sie in Betracht ziehen, dass das Fragment / die Aktivität die Drawables auf der Grundlage der Daten jongliert, über die das verfügt ViewModel. Angenommen, Sie müssen verschiedene Drawables in einer Ansicht für den Ein / Aus-Status anzeigen - es ist das ViewModel, das den (wahrscheinlich booleschen) Status enthalten sollte, aber es ist ViewAufgabe des Drawables, das Drawable entsprechend auszuwählen.

Mit DataBinding geht das ganz einfach :

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

Wenn Sie mehr Status und Drawables haben, können Sie einen benutzerdefinierten BindingAdapter schreiben , der beispielsweise einen EnumWert in übersetzt, um unhandliche Logik in der Layoutdatei zu vermeidenR.drawable.* (z. B. Kartenanzüge)

Oder Sie benötigen die ContextKomponente für eine Komponente, die Sie in Ihrem verwenden ViewModel. Erstellen Sie dann die Komponente außerhalb von ViewModelund übergeben Sie sie. Sie können DI oder Singletons verwenden oder die Contextabhängige Komponente direkt vor dem Initialisieren von ViewModelin Fragment/ erstellen Activity.

Warum sich die Mühe machen: Contextist eine Android-spezifische Sache, und abhängig von denen in ViewModels ist eine schlechte Praxis: Sie stehen Unit-Tests im Wege. Auf der anderen Seite haben Sie Ihre eigenen Komponenten- / Serviceschnittstellen vollständig unter Ihrer Kontrolle, sodass Sie sie zum Testen leicht verspotten können.

Ivan Bartsov
quelle
5

hat einen Verweis auf den Anwendungskontext, der jedoch androidspezifischen Code enthält

Gute Nachrichten, Sie können Mockito.mock(Context.class)den Kontext verwenden und dafür sorgen, dass er in Tests zurückgibt, was Sie wollen!

Verwenden ViewModelSie also einfach a wie gewohnt und geben Sie ihm den ApplicationContext über ViewModelProviders.Factory wie gewohnt.

EpicPandaForce
quelle
3

Sie können über getApplication().getApplicationContext()das ViewModel auf den Anwendungskontext zugreifen . Dies ist, was Sie benötigen, um auf Ressourcen, Einstellungen usw. zuzugreifen.

Alessandro Crugnola
quelle
Ich denke, meine Frage einzugrenzen. Ist es schlecht, eine Kontextreferenz im Ansichtsmodell zu haben (wirkt sich dies nicht auf das Testen aus?) Und würde die Verwendung der AndroidViewModel-Klasse Dagger in irgendeiner Weise beeinflussen? Ist es nicht an den Aktivitätslebenszyklus gebunden? Ich benutze Dolch, um den Lebenszyklus von Komponenten zu steuern
Vincent Williams
14
Die ViewModelKlasse hat die getApplicationMethode nicht.
Beroal
4
Nein, aber AndroidViewModeltut
4Oh4
1
Sie müssen die Anwendungsinstanz jedoch in ihrem Konstruktor übergeben. Dies entspricht dem Zugriff auf die Anwendungsinstanz von dort aus
John Sardinha,
2
Es ist kein großes Problem, einen Anwendungskontext zu haben. Sie möchten keinen Aktivitäts- / Fragmentkontext haben, da Sie überfordert sind, wenn das Fragment / die Aktivität zerstört wird und das Ansichtsmodell immer noch einen Verweis auf den jetzt nicht vorhandenen Kontext enthält. Der APPLICATION-Kontext wird jedoch niemals zerstört, aber die VM hat immer noch einen Verweis darauf. Richtig? Können Sie sich ein Szenario vorstellen, in dem Ihre App beendet wird, das Viewmodel jedoch nicht? :)
user1713450
3

Sie sollten keine Android-bezogenen Objekte in Ihrem ViewModel verwenden, da das Motiv für die Verwendung eines ViewModel darin besteht, den Java-Code und den Android-Code zu trennen, damit Sie Ihre Geschäftslogik separat testen können und eine separate Schicht von Android-Komponenten und Ihre Geschäftslogik haben und Daten, Sie sollten keinen Kontext in Ihrem ViewModel haben, da dies zu Abstürzen führen kann

Rohit Sharma
quelle
2
Dies ist eine faire Beobachtung, aber einige der Backend-Bibliotheken erfordern immer noch Anwendungskontexte wie MediaStore. Die Antwort von 4gus71n unten erklärt, wie man Kompromisse eingeht.
Bryan W. Wagner
1
Ja, Sie können den Anwendungskontext verwenden, jedoch nicht den Kontext der Aktivitäten, da der Anwendungskontext während des gesamten Anwendungslebenszyklus gültig ist, der Aktivitätskontext jedoch nicht als Übergabe des Aktivitätskontexts an einen asynchronen Prozess zu Speicherverlusten führen kann. Der in meinem Beitrag erwähnte Kontext ist Aktivität Kontext. Sie sollten jedoch darauf achten, dass der Kontext nicht an einen asynchronen Prozess übergeben wird, auch wenn es sich um einen Anwendungskontext handelt.
Rohit Sharma
2

Ich hatte Probleme, SharedPreferencesdie ViewModelKlasse zu benutzen , also nahm ich den Rat aus den obigen Antworten und machte die folgenden AndroidViewModel. Jetzt sieht alles gut aus

Für die 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>"));
    }

}

Und in der 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
quelle
0

Ich habe es so erstellt:

@Module
public class ContextModule {

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

Und dann habe ich in AppComponent die ContextModule.class hinzugefügt:

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

Und dann habe ich den Kontext in mein ViewModel eingefügt:

@Inject
@Named("AppContext")
Context context;
loopidio
quelle
0

Verwenden Sie das folgende Muster:

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