Come ottenere il contesto in Android MVVM ViewModel


96

Sto cercando di implementare il pattern MVVM nella mia app Android. Ho letto che ViewModels non dovrebbe contenere codice specifico per Android (per semplificare i test), tuttavia ho bisogno di usare il contesto per varie cose (ottenere risorse da xml, inizializzare le preferenze, ecc.). Qual è il modo migliore per farlo? Ho visto che AndroidViewModelha un riferimento al contesto dell'applicazione, tuttavia contiene codice specifico di Android, quindi non sono sicuro che dovrebbe essere nel ViewModel. Anche quelli si collegano agli eventi del ciclo di vita dell'attività, ma sto usando il pugnale per gestire l'ambito dei componenti, quindi non sono sicuro di come ciò lo influenzerebbe. Sono nuovo nel pattern MVVM e nel Dagger, quindi qualsiasi aiuto è apprezzato!


Nel caso in cui qualcuno sta cercando di utilizzo AndroidViewModelma ottenere Cannot create instance exceptionallora si può fare riferimento alla mia questa risposta stackoverflow.com/a/62626408/1055241
gprathour

Non dovresti usare Context in un ViewModel, crea invece un UseCase per ottenere il Context da quel modo
Ruben Caster

Risposte:


79

Puoi usare un Applicationcontesto fornito da AndroidViewModel, dovresti estendere AndroidViewModelche è semplicemente un ViewModelche include un Applicationriferimento.


Ha funzionato come un fascino!
SPM

64

Per il modello di visualizzazione dei componenti dell'architettura Android,

Non è una buona pratica passare il contesto dell'attività al ViewModel dell'attività poiché è una perdita di memoria.

Quindi, per ottenere il contesto nel tuo ViewModel, la classe ViewModel dovrebbe estendere la classe del modello di visualizzazione Android . In questo modo puoi ottenere il contesto come mostrato nel codice di esempio qui sotto.

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

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

    //... ViewModel methods 

}

3
Perché non utilizzare direttamente il parametro dell'applicazione e un normale ViewModel? Non vedo alcun punto in "getApplication <Application> ()". Aggiunge solo boilerplate.
L'incredibile

Perché dovrebbe essere una perdita di memoria?
Ben Butterworth

Capisco, perché un'attività verrà distrutta più spesso del suo modello di visualizzazione (ad esempio quando lo schermo ruota). Sfortunatamente, la memoria non verrà rilasciata dalla garbage collection perché il modello di visualizzazione ha ancora un riferimento ad essa.
Ben Butterworth

52

Non è che ViewModels non debba contenere codice specifico per Android per rendere i test più facili, poiché è l'astrazione che rende i test più facili.

Il motivo per cui ViewModels non dovrebbe contenere un'istanza di Context o qualcosa di simile a Views o altri oggetti che trattengono un Context è perché ha un ciclo di vita separato rispetto a Activities e Fragments.

Ciò che intendo con questo è, diciamo che fai un cambio di rotazione sulla tua app. Questo fa sì che la tua attività e il tuo frammento si autodistruggano in modo da ricrearsi. ViewModel è pensato per persistere durante questo stato, quindi ci sono possibilità che si verifichino arresti anomali e altre eccezioni se sta ancora mantenendo una vista o un contesto per l'attività distrutta.

Per quanto riguarda il modo in cui dovresti fare quello che vuoi fare, MVVM e ViewModel funzionano molto bene con il componente Databinding di JetPack. Per la maggior parte delle cose per cui in genere si memorizza una stringa, un int o così via, è possibile utilizzare Databinding per fare in modo che le viste lo visualizzino direttamente, quindi non è necessario memorizzare il valore all'interno di ViewModel.

Ma se non si desidera l'associazione dati, è comunque possibile passare il contesto all'interno del costruttore o dei metodi per accedere alle risorse. Basta non tenere un'istanza di quel contesto all'interno del tuo ViewModel.


1
Sapevo che l'inclusione del codice specifico per Android richiedeva l'esecuzione di test di strumentazione, il che è molto più lento dei semplici test JUnit. Attualmente sto utilizzando l'associazione dati per i metodi di clic, ma non vedo come sarebbe utile ottenere risorse da xml o per le preferenze. Ho appena capito che, per le preferenze, avrei bisogno anche di un contesto all'interno del mio modello. Quello che sto facendo attualmente è che Dagger inietti il ​​contesto dell'applicazione (il modulo del contesto lo ottiene da un metodo statico all'interno della classe dell'applicazione)
Vincent Williams

@VincentWilliams Sì, l'utilizzo di un ViewModel aiuta ad astrarre il codice dai componenti dell'interfaccia utente, il che rende più facile condurre i test. Ma quello che sto dicendo è che il motivo principale per non includere alcun contesto, viste o simili non è dovuto a motivi di test, ma a causa del ciclo di vita di ViewModel che può aiutarti a evitare arresti anomali e altri errori. Per quanto riguarda l'associazione dati, questo può aiutarti con le risorse perché la maggior parte del tempo necessario per accedere alle risorse nel codice è dovuto alla necessità di applicare quella stringa, colore, dimenare nel tuo layout, che l'associazione dati può fare direttamente.
Jackey

3
se voglio attivare / disattivare il testo in una visualizzazione di testo in base a un modello di visualizzazione del modulo di valore, la stringa deve essere localizzata, quindi ho bisogno di ottenere risorse nel mio modello di visualizzazione, senza contesto come accederò alle risorse?
Srishti Roy,

3
@SrishtiRoy Se usi l'associazione dati, è facilmente possibile alternare il testo di TextView in base al valore del tuo viewmodel. Non è necessario accedere a un contesto all'interno del ViewModel perché tutto ciò avviene all'interno dei file di layout. Tuttavia, se devi usare un contesto all'interno del tuo ViewModel, dovresti considerare di usare AndroidViewModel invece di ViewModel. AndroidViewModel contiene il contesto dell'applicazione che puoi chiamare con getApplication (), quindi dovrebbe soddisfare le tue esigenze di contesto se il tuo ViewModel richiede un contesto.
Jackey

1
@Pacerier Hai frainteso lo scopo principale di ViewModel. È una questione di separazione delle preoccupazioni. Il ViewModel non dovrebbe mantenere i riferimenti a nessuna vista, poiché è responsabilità di mantenere i dati che vengono visualizzati dal livello di visualizzazione. I componenti dell'interfaccia utente, ovvero le visualizzazioni, vengono gestiti dal livello di visualizzazione e il sistema Android ricrea le visualizzazioni se necessario. Mantenere un riferimento alle vecchie viste entrerà in conflitto con questo comportamento e causerà perdite di memoria.
Jackey

16

Risposta breve: non farlo

Perché ?

Sconfigge l'intero scopo dei modelli di visualizzazione

Quasi tutto ciò che puoi fare nel modello di visualizzazione può essere fatto in attività / frammento utilizzando istanze di LiveData e vari altri approcci consigliati.


26
Perché allora esiste anche la classe AndroidViewModel?
Alex Berdnikov

1
@AlexBerdnikov Lo scopo di MVVM è isolare la vista (attività / frammento) da ViewModel anche più di MVP. Così sarà più facile testare.
hushed_voice

3
@free_style Grazie per il chiarimento, ma la domanda rimane: se non dobbiamo mantenere il contesto in ViewModel, perché esiste anche la classe AndroidViewModel? Il suo scopo è fornire il contesto dell'applicazione, non è vero?
Alex Berdnikov

7
@AlexBerdnikov L'utilizzo del contesto Activity all'interno del viewmodel può causare perdite di memoria. Quindi, utilizzando AndroidViewModel Class ti verrà fornito dal contesto dell'applicazione che (si spera) non causerà alcuna perdita di memoria. Quindi l'utilizzo di AndroidViewModel potrebbe essere migliore rispetto al passaggio del contesto dell'attività. Ma ancora farlo renderà difficili i test. Questa è la mia opinione.
hushed_voice

1
Non riesco ad accedere al file dalla cartella res / raw dal repository?
Fugogugo

15

Quello che ho finito per fare invece di avere un contesto direttamente nel ViewModel, ho creato classi provider come ResourceProvider che mi avrebbero dato le risorse di cui avevo bisogno e ho inserito quelle classi provider nel mio ViewModel


1
Sto usando ResourcesProvider con Dagger in AppModule. È un buon approccio per ottenere il contesto per ResourcesProvider o AndroidViewModel è meglio ottenere il contesto per le risorse?
Usman Rana

@ Vincent: come utilizzare resourceProvider per ottenere Drawable all'interno di ViewModel?
Bulma

@Vegeta Aggiungeresti un metodo come getDrawableRes(@DrawableRes int id)all'interno della classe ResourceProvider
Vincent Williams

1
Questo va contro l'approccio Clean Architecture che afferma che le dipendenze del framework non dovrebbero oltrepassare i confini nella logica di dominio (ViewModels).
IgorGanapolsky

1
Le VM @IgorGanapolsky non sono esattamente logica di dominio. La logica del dominio sono altre classi come gli interattori e i repository per citarne alcuni. Le VM rientrano nella categoria "collante" poiché interagiscono con il tuo dominio, ma non direttamente. Se le tue VM fanno parte del tuo dominio, dovresti riconsiderare come stai utilizzando il pattern poiché stai dando loro troppe responsabilità.
mradzinski

9

TL; DR: Inietta il contesto dell'applicazione tramite Dagger nei tuoi ViewModels e usalo per caricare le risorse. Se è necessario caricare immagini, passare l'istanza View tramite argomenti dai metodi Databinding e utilizzare tale contesto View.

L'MVVM è una buona architettura ed è sicuramente il futuro dello sviluppo di Android, ma ci sono un paio di cose che sono ancora verdi. Prendiamo ad esempio la comunicazione dei livelli in un'architettura MVVM, ho visto diversi sviluppatori (sviluppatori molto noti) utilizzare LiveData per comunicare i diversi livelli in modi diversi. Alcuni di loro usano LiveData per comunicare ViewModel con l'interfaccia utente, ma poi usano interfacce di callback per comunicare con i repository, oppure hanno Interactors / UseCases e usano LiveData per comunicare con loro. Il punto qui è che non tutto è ancora definito al 100% .

Detto questo, il mio approccio con il tuo problema specifico è avere il contesto di un'applicazione disponibile tramite DI da utilizzare nei miei ViewModels per ottenere cose come String dal mio strings.xml

Se ho a che fare con il caricamento delle immagini, provo a passare attraverso gli oggetti View dai metodi dell'adattatore Databinding e utilizzo il contesto della vista per caricare le immagini. Perché? perché alcune tecnologie (ad esempio Glide) possono incorrere in problemi se utilizzi il contesto dell'applicazione per caricare le immagini.

Spero che sia d'aiuto!


5
TL; DR dovrebbe essere al top
Jacques Koorts

1
La ringrazio per la risposta. Tuttavia, perché dovresti usare dagger per iniettare il contesto se potessi far estendere il tuo viewmodel da androidviewmodel e utilizzare il contesto integrato fornito dalla classe stessa? Soprattutto considerando la quantità ridicola di codice boilerplate per far funzionare insieme dagger e MVVM, l'altra soluzione sembra molto più chiara. Cosa ne pensi di questo?
Josip Domazet

8

Come altri hanno già detto, c'è da AndroidViewModelcui puoi derivare per ottenere l'app, Contextma da quello che raccolgo nei commenti, stai cercando di manipolare @drawablei messaggi di posta elettronica dall'interno del tuo ViewModelche sconfigge lo scopo MVVM.

In generale, la necessità di avere una Contextnella vostra ViewModelsuggerisce quasi universalmente si dovrebbe considerare ripensare come si divide la logica tra le Views e ViewModels.

Invece di dover ViewModelrisolvere i drawable e fornirli all'attività / frammento, considera che il frammento / attività manipoli i drawables in base ai dati posseduti dal file ViewModel. Supponiamo che tu abbia bisogno di diversi drawable da visualizzare in una vista per lo stato on / off - è lo stato ViewModelche dovrebbe contenere lo stato (probabilmente booleano) ma è Viewcompito di selezionare il drawable di conseguenza.

Può essere fatto abbastanza facilmente con DataBinding :

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

Se hai più stati e drawables, per evitare logiche ingombranti nel file di layout puoi scrivere un BindingAdapter personalizzato che traduca, ad esempio, un Enumvalore in R.drawable.*(ad esempio semi di carte)

O forse hai bisogno Contextdi qualche componente che usi all'interno del tuo ViewModel- quindi, crea il componente all'esterno di ViewModele passalo dentro. Puoi usare DI, o singleton, o creare il Contextcomponente -dependent subito prima di inizializzare l' ViewModelin Fragment/ Activity.

Perché preoccuparsi: Contextè una cosa specifica di Android e dipendere da quelli in ViewModels è una cattiva pratica: ostacolano i test unitari. D'altra parte, le interfacce dei tuoi componenti / servizi sono completamente sotto il tuo controllo, quindi puoi facilmente deriderle per i test.


5

ha un riferimento al contesto dell'applicazione, tuttavia che contiene codice specifico di Android

Buone notizie, puoi usare Mockito.mock(Context.class)e fare in modo che il contesto restituisca quello che vuoi nei test!

Quindi usa ViewModelcome faresti normalmente e dagli ApplicationContext tramite ViewModelProviders.Factory come faresti normalmente.


3

è possibile accedere al contesto dell'applicazione getApplication().getApplicationContext()dall'interno di ViewModel. Questo è ciò di cui hai bisogno per accedere a risorse, preferenze, ecc.


Immagino di restringere la mia domanda. È sbagliato avere un riferimento di contesto all'interno del viewmodel (non influisce sui test?) E l'utilizzo della classe AndroidViewModel influirebbe in qualche modo su Dagger? Non è legato al ciclo di vita dell'attività? Sto usando Dagger per controllare il ciclo di vita dei componenti
Vincent Williams

14
La ViewModelclasse non ha il getApplicationmetodo.
beroal

4
No, ma AndroidViewModello fa
4Oh4

1
Ma è necessario passare l'istanza dell'applicazione nel suo costruttore, è proprio come accedere all'istanza dell'applicazione da essa
John Sardinha

2
Non è un grosso problema avere il contesto dell'applicazione. Non vuoi avere un contesto attività / frammento perché sei borked se il frammento / attività viene distrutto e il modello di visualizzazione ha ancora un riferimento al contesto ora inesistente. Ma il contesto APPLICATION non verrà mai distrutto ma la VM ha ancora un riferimento ad esso. Destra? Riesci a immaginare uno scenario in cui la tua app esce ma il Viewmodel no? :)
user1713450

3

Non dovresti utilizzare oggetti correlati ad Android nel tuo ViewModel poiché il motivo dell'utilizzo di un ViewModel è separare il codice java e il codice Android in modo da poter testare la tua logica aziendale separatamente e avrai un livello separato di componenti Android e la tua logica aziendale e dati, non dovresti avere contesto nel tuo ViewModel in quanto potrebbe causare arresti anomali


2
Questa è un'osservazione corretta, ma alcune delle librerie di backend richiedono ancora contesti applicativi, come MediaStore. La risposta di 4gus71n di seguito spiega come scendere a compromessi.
Bryan W. Wagner

1
Sì, è possibile utilizzare il contesto dell'applicazione ma non il contesto delle attività, poiché il contesto dell'applicazione è presente per tutto il ciclo di vita dell'applicazione ma non il contesto dell'attività poiché il passaggio del contesto dell'attività a qualsiasi processo asincrono può causare perdite di memoria. Contesto, ma dovresti comunque fare attenzione a non passare il contesto a nessun processo asincrono anche se è il contesto delle applicazioni.
Rohit Sharma

2

Avevo problemi a ottenere SharedPreferencesquando usavo la ViewModelclasse, quindi ho seguito il consiglio dalle risposte sopra e ho fatto quanto segue usando AndroidViewModel. Tutto sembra fantastico adesso

Per il 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>"));
    }

}

E nel 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;
    }
}

0

L'ho creato in questo modo:

@Module
public class ContextModule {

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

E poi ho appena aggiunto in AppComponent il ContextModule.class:

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

E poi ho inserito il contesto nel mio ViewModel:

@Inject
@Named("AppContext")
Context context;

0

Usa lo schema seguente:

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

0

Il problema con l'inserimento di un contesto nel ViewModel è che il contesto può cambiare in qualsiasi momento, a seconda della rotazione dello schermo, della modalità notturna o della lingua del sistema e qualsiasi risorsa restituita può cambiare di conseguenza. La restituzione di un semplice ID risorsa causa problemi per parametri aggiuntivi, come le sostituzioni getString. Restituire un risultato di alto livello e spostare la logica di rendering nell'attività rende più difficile il test.

La mia soluzione è fare in modo che ViewModel generi e restituisca una funzione che viene successivamente eseguita attraverso il contesto dell'attività. Lo zucchero sintattico di Kotlin lo rende incredibilmente facile!

ViewModel.kt:

// connectedStatus holds a function that calls Context methods
// `this` can be elided
val connectedStatus = MutableLiveData<Context.() -> String> {
  // initial value
  this.getString(R.string.connectionStatusWaiting)
}
connectedStatus.postValue {
  this.getString(R.string.connectionStatusConnected, brand)
}
Activity.kt  // is a Context

override fun onCreate(_: Bundle?) {
  connectionViewModel.connectedStatus.observe(this) { it ->
   // runs the posted value with the given Context receiver
   txtConnectionStatus.text = this.run(it)
  }
}

Ciò consente a ViewModel di contenere tutta la logica per il calcolo delle informazioni visualizzate, verificata da test unitari, con l'attività che è una rappresentazione molto semplice senza logica interna per nascondere i bug.


E per abilitare il supporto dell'associazione dati, è sufficiente aggiungere un semplice adattatore Binding in questo modo:@BindingAdapter("android:text") fun setText(view: TextView, value: Context.() -> String) { view.text = view.context.run(value) }
hufman
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.