Cómo obtener contexto en Android MVVM ViewModel

90

Estoy tratando de implementar el patrón MVVM en mi aplicación de Android. He leído que ViewModels no debe contener un código específico de Android (para facilitar las pruebas), sin embargo, necesito usar el contexto para varias cosas (obtener recursos de xml, inicializar preferencias, etc.). ¿Cuál es la mejor manera de hacer esto? Vi que AndroidViewModeltiene una referencia al contexto de la aplicación, sin embargo, contiene código específico de Android, así que no estoy seguro de si debería estar en ViewModel. También esos se relacionan con los eventos del ciclo de vida de la actividad, pero estoy usando dagger para administrar el alcance de los componentes, por lo que no estoy seguro de cómo eso lo afectaría. Soy nuevo en el patrón MVVM y Dagger, ¡así que agradecemos cualquier ayuda!

Vincent Williams
fuente
En caso de que alguien intente usarlo AndroidViewModelpero lo obtenga Cannot create instance exception, puede consultar esta respuesta stackoverflow.com/a/62626408/1055241
gprathour
No debe usar Context en un ViewModel, cree un UseCase en su lugar para obtener el Context de esa manera
Ruben Caster

Respuestas:

71

Puede utilizar un Applicationcontexto proporcionado por AndroidViewModel, debe ampliar, AndroidViewModelque es simplemente un ViewModelque incluye una Applicationreferencia.

Arrendajo
fuente
¡Trabajado como un encanto!
SPM
¿Alguien podría mostrar esto en código? Estoy en Java
Biswas Khayargoli
55

Para el modelo de vista de componentes de arquitectura de Android,

No es una buena práctica pasar su contexto de actividad al modelo de vista de la actividad, ya que es una pérdida de memoria.

Por lo tanto, para obtener el contexto en su ViewModel, la clase ViewModel debería extender la clase de modelo de vista de Android . De esa manera, puede obtener el contexto como se muestra en el código de ejemplo a continuación.

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

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

    //... ViewModel methods 

}
devDeejay
fuente
2
¿Por qué no utilizar directamente el parámetro de la aplicación y un ViewModel normal? No veo ningún sentido en "getApplication <Application> ()". Simplemente agrega texto estándar.
El increíble
50

No es que ViewModels no deba contener código específico de Android para facilitar las pruebas, ya que es la abstracción lo que facilita las pruebas.

La razón por la que ViewModels no debería contener una instancia de Contexto o algo como Vistas u otros objetos que se aferran a un Contexto es porque tiene un ciclo de vida diferente al de Actividades y Fragmentos.

Lo que quiero decir con esto es que digamos que haces un cambio de rotación en tu aplicación. Esto hace que su Actividad y Fragmento se destruyan a sí mismos para que se vuelvan a crear. ViewModel está destinado a persistir durante este estado, por lo que existe la posibilidad de que se produzcan bloqueos y otras excepciones si todavía tiene una Vista o Contexto de la Actividad destruida.

En cuanto a cómo debe hacer lo que quiere hacer, MVVM y ViewModel funcionan muy bien con el componente Databinding de JetPack. Para la mayoría de las cosas para las que normalmente almacenaría un String, int, o etc., puede usar Databinding para que las Vistas lo muestren directamente, por lo que no es necesario almacenar el valor dentro de ViewModel.

Pero si no desea el enlace de datos, aún puede pasar el contexto dentro del constructor o los métodos para acceder a los recursos. Simplemente no guarde una instancia de ese contexto dentro de su ViewModel.

Jackey
fuente
1
Tenía entendido que la inclusión de código específico de Android requería la ejecución de pruebas de instrumentación, que son mucho más lentas que las pruebas simples de JUnit. Actualmente estoy usando Databinding para métodos de clic, pero no veo cómo ayudaría a obtener recursos de xml o para preferencias. Me acabo de dar cuenta de que para las preferencias, también necesitaría un contexto dentro de mi modelo. Lo que estoy haciendo actualmente es hacer que Dagger inyecte el contexto de la aplicación (el módulo de contexto lo obtiene de un método estático dentro de la clase de la aplicación)
Vincent Williams
@VincentWilliams Sí, usar un ViewModel ayuda a abstraer su código de los componentes de la interfaz de usuario, lo que le facilita la realización de pruebas. Pero lo que estoy diciendo es que la razón principal para no incluir ningún Contexto, Vistas o similares no es por razones de prueba, sino por el ciclo de vida de ViewModel, que puede ayudarlo a evitar fallas y otros errores. En cuanto al enlace de datos, esto puede ayudarlo con los recursos porque la mayor parte del tiempo que necesita para acceder a los recursos en el código se debe a la necesidad de aplicar esa Cadena, color, dimensión en su diseño, lo que el enlace de datos puede hacer directamente.
Jackey
Oh, está bien, veo lo que quieres decir, pero el enlace de datos no me ayudará en este caso, ya que necesito acceder a cadenas para usarlas en el modelo (estas podrían colocarse en una clase de constantes en lugar de xml, supongo) y también para inicializar SharedPreferences
Vincent Williams
3
si quiero alternar el texto en una vista de texto en función de un modelo de vista de formulario de valor, la cadena debe estar localizada, por lo que necesito obtener recursos en mi modelo de vista, sin contexto, ¿cómo accederé a los recursos?
Srishti Roy
3
@SrishtiRoy Si usa el enlace de datos, es fácilmente posible alternar el texto de un TextView en función del valor de su modelo de vista. No es necesario acceder a un contexto dentro de su ViewModel porque todo esto sucede dentro de los archivos de diseño. Sin embargo, si debe usar un contexto dentro de su ViewModel, entonces debería considerar usar AndroidViewModel en lugar de ViewModel. AndroidViewModel contiene el contexto de la aplicación que puede llamar con getApplication (), por lo que debería satisfacer sus necesidades de contexto si su ViewModel requiere un contexto.
Jackey
15

Respuesta corta: no hagas esto

Por qué ?

Derrota todo el propósito de los modelos de vista.

Casi todo lo que puede hacer en el modelo de vista se puede hacer en actividad / fragmento utilizando instancias de LiveData y varios otros enfoques recomendados.

humble_wolf
fuente
21
¿Por qué entonces existe la clase AndroidViewModel?
Alex Berdnikov
1
@AlexBerdnikov El propósito de MVVM es aislar la vista (Actividad / Fragmento) de ViewModel incluso más que MVP. Para que sea más fácil de probar.
hushed_voice
3
@free_style Gracias por la aclaración, pero la pregunta sigue en pie: si no debemos mantener el contexto en ViewModel, ¿por qué existe la clase AndroidViewModel? Todo su propósito es proporcionar contexto de aplicación, ¿no es así?
Alex Berdnikov
6
@AlexBerdnikov El uso del contexto de actividad dentro del modelo de vista puede provocar pérdidas de memoria. Entonces, al usar AndroidViewModel Class, el contexto de la aplicación lo proporcionará, lo que no causará (con suerte) ninguna pérdida de memoria. Por lo tanto, usar AndroidViewModel podría ser mejor que pasarle el contexto de la actividad. Pero seguir haciéndolo dificultará las pruebas. Esta es mi opinión sobre ella.
hushed_voice
1
¿No puedo acceder al archivo desde la carpeta res / raw desde el repositorio?
Fugogugo
14

Lo que terminé haciendo en lugar de tener un contexto directamente en ViewModel, hice clases de proveedor como ResourceProvider que me darían los recursos que necesito, y tuve esas clases de proveedor inyectadas en mi ViewModel

Vincent Williams
fuente
1
Estoy usando ResourcesProvider con Dagger en AppModule. ¿Es ese un buen enfoque para obtener el contexto de ResourcesProvider o AndroidViewModel es mejor para obtener el contexto de los recursos?
Usman Rana
@Vincent: ¿Cómo usar resourceProvider para obtener Drawable dentro de ViewModel?
HoangVu
@Vegeta Agregaría un método como getDrawableRes(@DrawableRes int id)dentro de la clase ResourceProvider
Vincent Williams
1
Esto va en contra del enfoque de Arquitectura limpia, que establece que las dependencias del marco no deben cruzar los límites hacia la lógica del dominio (ViewModels).
IgorGanapolsky
1
@IgorGanapolsky Las máquinas virtuales no son exactamente lógica de dominio. La lógica de dominio son otras clases, como interactores y repositorios, por nombrar algunos. Las máquinas virtuales entran en la categoría de "pegamento" ya que interactúan con su dominio, pero no directamente. Si sus VM son parte de su dominio, entonces debería reconsiderar cómo está usando el patrón, ya que les está dando demasiada responsabilidad.
mradzinski
8

TL; DR: Inyecte el contexto de la aplicación a través de Dagger en sus ViewModels y utilícelo para cargar los recursos. Si necesita cargar imágenes, pase la instancia de View a través de argumentos de los métodos de enlace de datos y use ese contexto de View.

El MVVM es una buena arquitectura y definitivamente es el futuro del desarrollo de Android, pero hay un par de cosas que aún son ecológicas. Tomemos, por ejemplo, la comunicación de capas en una arquitectura MVVM, he visto a diferentes desarrolladores (desarrolladores muy conocidos) usar LiveData para comunicar las diferentes capas de diferentes maneras. Algunos de ellos usan LiveData para comunicar el ViewModel con la UI, pero luego usan interfaces de devolución de llamada para comunicarse con los Repositories, o tienen Interactors / UseCases y usan LiveData para comunicarse con ellos. El punto aquí es que no todo está 100% definido todavía .

Dicho esto, mi enfoque con su problema específico es tener el contexto de una aplicación disponible a través de DI para usar en mis ViewModels para obtener cosas como String de mis strings.xml

Si estoy tratando con la carga de imágenes, trato de pasar a través de los objetos Ver desde los métodos del adaptador de enlace de datos y uso el contexto de la Vista para cargar las imágenes. ¿Por qué? porque algunas tecnologías (por ejemplo, Glide) pueden tener problemas si usa el contexto de la Aplicación para cargar imágenes.

¡Espero eso ayude!

4gus71n
fuente
5
TL; DR debería estar en la parte superior
Jacques Koorts
1
Gracias por su respuesta. Sin embargo, ¿por qué usarías dagger para inyectar el contexto si pudieras hacer que tu modelo de vista se extienda desde androidviewmodel y usar el contexto incorporado que la clase misma proporciona? Especialmente considerando la cantidad ridícula de código repetitivo para hacer que dagger y MVVM funcionen juntos, la otra solución parece mucho más clara en mi opinión. ¿Qué piensas sobre esto?
Josip Domazet
7

Como otros han mencionado, hay algo de AndroidViewModello que puede derivar para obtener la aplicación, Contextpero por lo que recopilé en los comentarios, está tratando de manipular los mensajes de correo electrónico @drawabledesde su interior, lo ViewModelque frustra el propósito de MVVM.

En general, la necesidad de tener un Contexten su ViewModelcasi universal sugiere que debería considerar repensar cómo divide la lógica entre sus Viewy ViewModels.

En lugar de ViewModelresolver los elementos dibujables y alimentarlos con la Actividad / Fragmento, considere hacer que el Fragmento / Actividad haga malabarismos con los elementos dibujables en función de los datos que posee ViewModel. Digamos que necesita que se muestren diferentes elementos de diseño en una vista para el estado de encendido / apagado; es el ViewModelque debería contener el estado (probablemente booleano), pero Viewes asunto de ellos seleccionar el elemento de diseño en consecuencia.

Se puede hacer bastante fácil con DataBinding :

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

Si tiene más estados y elementos de diseño, para evitar una lógica difícil de manejar en el archivo de diseño, puede escribir un BindingAdapter personalizado que traduzca, digamos, un Enumvalor en R.drawable.*(por ejemplo, juegos de cartas)

O tal vez lo que necesita el Contextpor algún componente que se utiliza dentro de su ViewModel- a continuación, crear el componente fuera del mismo ViewModely pasarlo en Puede utilizar DI, o únicos, o crear el. ContextDerecho componente dependiente antes de inicializar el ViewModelen Fragment/ Activity.

Por qué molestarse: Contextes algo específico de Android, y depender de los de ViewModels es una mala práctica: se interponen en el camino de las pruebas unitarias. Por otro lado, sus propias interfaces de componentes / servicios están completamente bajo su control, por lo que puede simularlas fácilmente para probarlas.

Ivan Bartsov
fuente
5

tiene una referencia al contexto de la aplicación, sin embargo, contiene código específico de Android

Buenas noticias, puede usar Mockito.mock(Context.class)y hacer que el contexto devuelva lo que quiera en las pruebas.

Así que simplemente use un ViewModelcomo lo haría normalmente, y dele el ApplicationContext a través de ViewModelProviders.Factory como lo haría normalmente.

EpicPandaForce
fuente
3

puede acceder al contexto de la aplicación desde getApplication().getApplicationContext()ViewModel. Esto es lo que necesita para acceder a recursos, preferencias, etc.

Alessandro Crugnola
fuente
Supongo que para reducir mi pregunta. ¿Es malo tener una referencia de contexto dentro del modelo de vista (¿no afecta esto a las pruebas?) Y el uso de la clase AndroidViewModel afectaría a Dagger de alguna manera? ¿No está ligado al ciclo de vida de la actividad? Estoy usando Dagger para controlar el ciclo de vida de los componentes
Vincent Williams
14
La ViewModelclase no tiene el getApplicationmétodo.
beroal
4
No, pero lo AndroidViewModelhace
4Oh4
1
Pero debe pasar la instancia de la aplicación en su constructor, es lo mismo que acceder a la instancia de la aplicación desde ella
John Sardinha
2
No supone un gran problema tener un contexto de aplicación. No desea tener un contexto de actividad / fragmento porque está fastidiado si el fragmento / actividad se destruye y el modelo de vista todavía tiene una referencia al contexto ahora inexistente. Pero nunca se destruirá el contexto de APLICACIÓN, pero la máquina virtual todavía tiene una referencia a él. ¿Derecho? ¿Te imaginas un escenario en el que tu aplicación salga pero el modelo de vista no? :)
user1713450
3

No debe usar objetos relacionados con Android en su ViewModel, ya que el motivo de usar un ViewModel es separar el código java y el código de Android para que pueda probar su lógica comercial por separado y tendrá una capa separada de componentes Android y su lógica comercial y datos, no debe tener contexto en su ViewModel, ya que puede provocar bloqueos

Rohit Sharma
fuente
2
Esta es una observación justa, pero algunas de las bibliotecas de backend aún requieren contextos de aplicación, como MediaStore. La respuesta de 4gus71n a continuación explica cómo comprometerse.
Bryan W. Wagner
1
Sí, puede usar el contexto de la aplicación pero no el contexto de las actividades, ya que el contexto de la aplicación vive durante todo el ciclo de vida de la aplicación, pero no el contexto de la actividad, ya que pasar el contexto de la actividad a cualquier proceso asíncrono puede provocar pérdidas de memoria. El contexto mencionado en mi publicación es Actividad Contexto, pero aún así debe tener cuidado de no pasar contexto a ningún proceso asincrónico, incluso si es el contexto de una aplicación.
Rohit Sharma
2

Estaba teniendo problemas para SharedPreferencesusar la ViewModelclase, así que seguí el consejo de las respuestas anteriores e hice lo siguiente usando AndroidViewModel. Todo se ve genial ahora

Para el 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>"));
    }

}

Y en el 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
fuente
0

Lo creé de esta manera:

@Module
public class ContextModule {

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

Y luego acabo de agregar en AppComponent el ContextModule.class:

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

Y luego inyecté el contexto en mi ViewModel:

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

Utilice el siguiente patrón:

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