Estou tentando implementar o padrão MVVM em meu aplicativo Android. Eu li que ViewModels não deve conter nenhum código específico do Android (para tornar o teste mais fácil), no entanto, preciso usar o contexto para várias coisas (obter recursos do xml, inicializar preferências, etc). Qual é a melhor maneira de fazer isso? Eu vi que AndroidViewModel
tem uma referência ao contexto do aplicativo, no entanto, contém o código específico do Android, então não tenho certeza se deve estar no ViewModel. Além disso, esses se ligam aos eventos do ciclo de vida da atividade, mas estou usando o punhal para gerenciar o escopo dos componentes, então não tenho certeza de como isso o afetaria. Eu sou novo no padrão MVVM e no Dagger, então qualquer ajuda é apreciada!
fonte
AndroidViewModel
mas conseguindoCannot create instance exception
, você pode consultar minha esta resposta stackoverflow.com/a/62626408/1055241Respostas:
Você pode usar um
Application
contexto que é fornecido peloAndroidViewModel
, você deve estender,AndroidViewModel
que é simplesmente umViewModel
que inclui umaApplication
referência.fonte
Para o modelo de visualização de componentes de arquitetura do Android,
Não é uma boa prática passar seu Activity Context para o Activity's ViewModel como um vazamento de memória.
Portanto, para obter o contexto em seu ViewModel, a classe ViewModel deve estender a classe Android View Model . Dessa forma, você pode obter o contexto conforme mostrado no código de exemplo abaixo.
class ActivityViewModel(application: Application) : AndroidViewModel(application) { private val context = getApplication<Application>().applicationContext //... ViewModel methods }
fonte
Não é que ViewModels não deva conter código específico do Android para tornar o teste mais fácil, já que é a abstração que torna o teste mais fácil.
A razão pela qual ViewModels não deve conter uma instância de Context ou qualquer coisa como Views ou outros objetos que se prendem a um Context é porque ele tem um ciclo de vida separado do Activities and Fragments.
O que quero dizer com isso é, digamos que você faça uma mudança de rotação em seu aplicativo. Isso faz com que sua Atividade e Fragmento se destruam e se recriem. ViewModel deve persistir durante este estado, então há chances de travamentos e outras exceções acontecendo se ele ainda estiver mantendo uma View ou Context para a Activity destruída.
Quanto a como você deve fazer o que deseja, MVVM e ViewModel funcionam muito bem com o componente Databinding do JetPack. Para a maioria das coisas para as quais você normalmente armazenaria uma String, int ou etc, você pode usar Databinding para fazer as Views exibi-las diretamente, sem a necessidade de armazenar o valor dentro de ViewModel.
Mas se você não quiser Databinding, você ainda pode passar o Contexto dentro do construtor ou métodos para acessar os Recursos. Apenas não mantenha uma instância desse Context dentro de seu ViewModel.
fonte
Resposta curta - Não faça isso
Por quê ?
Isso vai contra todo o propósito dos modelos de visão
Quase tudo que você pode fazer no modelo de exibição pode ser feito em atividade / fragmento usando instâncias do LiveData e várias outras abordagens recomendadas.
fonte
O que acabei fazendo, em vez de ter um Context diretamente no ViewModel, criei classes de provedor, como ResourceProvider, que me dariam os recursos de que preciso, e coloquei essas classes de provedor em meu ViewModel
fonte
getDrawableRes(@DrawableRes int id)
dentro da classe ResourceProviderTL; DR: injete o contexto do aplicativo por meio do Dagger em seus ViewModels e use-o para carregar os recursos. Se precisar carregar imagens, passe a instância de View por meio de argumentos dos métodos Databinding e use esse contexto de View.
O MVVM é uma boa arquitetura e é definitivamente o futuro do desenvolvimento do Android, mas há algumas coisas que ainda são verdes. Tomemos por exemplo a comunicação de camadas em uma arquitetura MVVM, já vi diferentes desenvolvedores (desenvolvedores muito conhecidos) usarem LiveData para comunicar as diferentes camadas de maneiras diferentes. Alguns deles usam LiveData para comunicar o ViewModel com a UI, mas usam interfaces de retorno de chamada para se comunicar com os Repositórios ou têm Interactors / UseCases e usam LiveData para se comunicar com eles. O ponto aqui é que nem tudo está 100% definido ainda .
Dito isso, minha abordagem com seu problema específico é ter um contexto de aplicativo disponível por meio de DI para usar em meus ViewModels para obter coisas como String de meu strings.xml
Se estou lidando com o carregamento de imagens, tento passar pelos objetos View dos métodos do adaptador Databinding e usar o contexto da View para carregar as imagens. Por quê? porque algumas tecnologias (por exemplo, Glide) podem ter problemas se você usar o contexto do aplicativo para carregar imagens.
Espero que ajude!
fonte
Como outros mencionaram, há o
AndroidViewModel
que você pode derivar para obter o aplicativo,Context
mas pelo que reuni nos comentários, você está tentando manipular@drawable
s de dentro do seu, oViewModel
que anula o propósito do MVVM.Em geral, a necessidade de ter um
Context
em seuViewModel
quase universalmente sugere que você deve considerar repensar como você divide a lógica entre seusView
eViewModels
.Em vez de
ViewModel
resolver drawables e alimentá-los para a Activity / Fragment, considere fazer com que o Fragment / Activity faça malabarismos com os drawables com base nos dados possuídos peloViewModel
. Digamos que você precise que diferentes drawables sejam exibidos em uma visualização para o estado ativado / desativado - é oViewModel
que deve manter o estado (provavelmente booleano), mas é aView
função do drawable selecionar o drawable de acordo.Isso pode ser feito de forma bastante fácil com DataBinding :
<ImageView ... app:src="@{viewModel.isOn ? @drawable/switch_on : @drawable/switch_off}" />
Se você tiver mais estados e drawables, para evitar lógica complicada no arquivo de layout, você pode escrever um BindingAdapter personalizado que traduz, digamos, um
Enum
valor emR.drawable.*
(por exemplo, naipes)Ou talvez você precise do
Context
para algum componente que usa dentro do seuViewModel
- então, crie o componente fora doViewModel
e passe-o. Você pode usar DI, ou singletons, ou criar oContext
componente -dependente antes de inicializar oViewModel
inFragment
/Activity
.Por que se preocupar:
Context
é uma coisa específica do Android, e depender daqueles emViewModel
s é uma prática ruim: eles atrapalham o teste de unidade. Por outro lado, suas próprias interfaces de componente / serviço estão totalmente sob seu controle para que você possa simular facilmente para teste.fonte
Boas notícias, você pode usar
Mockito.mock(Context.class)
e fazer com que o contexto retorne o que quiser nos testes!Portanto, apenas use um
ViewModel
como faria normalmente e forneça a ele o ApplicationContext por meio de ViewModelProviders.Factory como faria normalmente.fonte
você pode acessar o contexto do aplicativo
getApplication().getApplicationContext()
de dentro do ViewModel. É disso que você precisa para acessar recursos, preferências, etc.fonte
ViewModel
classe não possui ogetApplication
método.AndroidViewModel
fazVocê não deve usar objetos relacionados ao Android em seu ViewModel, pois o motivo de usar um ViewModel é separar o código Java e o código Android para que você possa testar sua lógica de negócios separadamente e você terá uma camada separada de componentes Android e sua lógica de negócios e dados, você não deve ter contexto em seu ViewModel, pois isso pode causar falhas
fonte
Eu estava tendo problemas
SharedPreferences
ao usar aViewModel
classe, então segui o conselho das respostas acima e fiz o seguinte usandoAndroidViewModel
. Tudo parece ótimo agoraPara o
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 no
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; } }
fonte
Eu criei desta forma:
E então acabei de adicionar no AppComponent o ContextModule.class:
@Component( modules = { ... ContextModule.class } ) public interface AppComponent extends AndroidInjector<BaseApplication> { ..... }
E então injetei o contexto em meu ViewModel:
fonte
Use o seguinte padrão:
fonte
O problema de injetar um Context no ViewModel é que o Context pode mudar a qualquer momento, dependendo da rotação da tela, modo noturno ou idioma do sistema, e quaisquer recursos retornados podem mudar de acordo. Retornar um ID de recurso simples causa problemas para parâmetros extras, como substituições getString. Retornar um resultado de alto nível e mover a lógica de renderização para a atividade torna mais difícil o teste.
Minha solução é fazer com que o ViewModel gere e retorne uma função que é posteriormente executada através do Contexto da Atividade. O açúcar sintático de Kotlin torna isso incrivelmente fácil!
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) } }
Isso permite que ViewModel mantenha toda a lógica de cálculo das informações exibidas, verificadas por testes de unidade, sendo a Activity uma representação muito simples, sem lógica interna para esconder bugs.
fonte
@BindingAdapter("android:text")
fun setText(view: TextView, value: Context.() -> String) {
view.text = view.context.run(value)
}