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 AndroidViewModel
tiene 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!
fuente
AndroidViewModel
pero lo obtengaCannot create instance exception
, puede consultar esta respuesta stackoverflow.com/a/62626408/1055241Respuestas:
Puede utilizar un
Application
contexto proporcionado porAndroidViewModel
, debe ampliar,AndroidViewModel
que es simplemente unViewModel
que incluye unaApplication
referencia.fuente
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 }
fuente
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.
fuente
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.
fuente
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
fuente
getDrawableRes(@DrawableRes int id)
dentro de la clase ResourceProviderTL; 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!
fuente
Como otros han mencionado, hay algo de
AndroidViewModel
lo que puede derivar para obtener la aplicación,Context
pero por lo que recopilé en los comentarios, está tratando de manipular los mensajes de correo electrónico@drawable
desde su interior, loViewModel
que frustra el propósito de MVVM.En general, la necesidad de tener un
Context
en suViewModel
casi universal sugiere que debería considerar repensar cómo divide la lógica entre susView
yViewModels
.En lugar de
ViewModel
resolver 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 poseeViewModel
. Digamos que necesita que se muestren diferentes elementos de diseño en una vista para el estado de encendido / apagado; es elViewModel
que debería contener el estado (probablemente booleano), peroView
es 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
Enum
valor enR.drawable.*
(por ejemplo, juegos de cartas)O tal vez lo que necesita el
Context
por algún componente que se utiliza dentro de suViewModel
- a continuación, crear el componente fuera del mismoViewModel
y pasarlo en Puede utilizar DI, o únicos, o crear el.Context
Derecho componente dependiente antes de inicializar elViewModel
enFragment
/Activity
.Por qué molestarse:
Context
es algo específico de Android, y depender de los deViewModel
s 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.fuente
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
ViewModel
como lo haría normalmente, y dele el ApplicationContext a través de ViewModelProviders.Factory como lo haría normalmente.fuente
puede acceder al contexto de la aplicación desde
getApplication().getApplicationContext()
ViewModel. Esto es lo que necesita para acceder a recursos, preferencias, etc.fuente
ViewModel
clase no tiene elgetApplication
método.AndroidViewModel
haceNo 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
fuente
Estaba teniendo problemas para
SharedPreferences
usar laViewModel
clase, así que seguí el consejo de las respuestas anteriores e hice lo siguiente usandoAndroidViewModel
. Todo se ve genial ahoraPara 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; } }
fuente
Lo creé de esta manera:
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:
fuente
Utilice el siguiente patrón:
fuente