Como obter o contexto no Android MVVM ViewModel

96

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 AndroidViewModeltem 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!

Vincent Williams
fonte
Caso alguém esteja tentando usar, AndroidViewModelmas conseguindo Cannot create instance exception, você pode consultar minha esta resposta stackoverflow.com/a/62626408/1055241
gprathour
Você não deve usar Context em um ViewModel, crie um UseCase para obter o Contexto dessa maneira
Ruben Caster

Respostas:

79

Você pode usar um Applicationcontexto que é fornecido pelo AndroidViewModel, você deve estender, AndroidViewModelque é simplesmente um ViewModelque inclui uma Applicationreferência.

Jay
fonte
Funcionou como um encanto!
SPM
64

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 

}
devDeejay
fonte
3
Por que não usar diretamente o parâmetro do aplicativo e um ViewModel normal? Não vejo sentido em "getApplication <Application> ()". Ele apenas adiciona clichês.
O incrível janeiro
Por que seria um vazamento de memória?
Ben Butterworth
Ah, entendo, porque uma atividade será destruída com mais frequência do que seu modelo de exibição (por exemplo, quando a tela está girando). Infelizmente, a memória não será liberada pela coleta de lixo porque o modelo de exibição ainda tem uma referência a ela.
Ben Butterworth
52

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.

Jackey
fonte
1
Foi meu entendimento que incluir o código específico do Android exigia a execução de testes de instrumentação, o que é muito mais lento do que os testes JUnit simples. Atualmente, estou usando Databinding para métodos de clique, mas não vejo como isso ajudaria a obter recursos de xml ou para preferências. Acabei de perceber que, para preferências, também precisaria de um contexto dentro do meu modelo. O que estou fazendo atualmente é fazer com que Dagger injete o contexto do aplicativo (o módulo de contexto o obtém de um método estático dentro da classe do aplicativo)
Vincent Williams
@VincentWilliams Sim, usar um ViewModel ajuda a abstrair seu código de seus componentes de IU, o que torna mais fácil para você realizar testes. Mas, o que estou dizendo é que o motivo principal para não incluir nenhum Contexto, Visualizações ou semelhantes não é por motivos de teste, mas por causa do ciclo de vida do ViewModel, que pode ajudá-lo a evitar travamentos e outros erros. Quanto à vinculação de dados, isso pode ajudá-lo com recursos porque na maioria das vezes você precisa acessar os recursos no código devido à necessidade de aplicar aquela String, cor, dimen em seu layout, que a vinculação de dados pode fazer diretamente.
Jackey
3
se eu quiser alternar o texto em um textview com base em um viewmodel de formulário de valor, a string precisa ser localizada, então preciso obter recursos em meu viewmodel, sem contexto, como irei acessar os recursos?
Srishti Roy
3
@SrishtiRoy Se você usa ligação de dados, é facilmente possível alternar o texto de um TextView com base no valor de seu modelo de visão. Não há necessidade de acessar um Context dentro de seu ViewModel porque tudo isso acontece dentro dos arquivos de layout. No entanto, se você deve usar um Context dentro de seu ViewModel, então você deve considerar o uso de AndroidViewModel em vez de ViewModel. AndroidViewModel contém o contexto do aplicativo que você pode chamar com getApplication (), de modo que deve satisfazer suas necessidades de contexto se seu ViewModel requer um contexto.
Jackey
1
@Pacerier Você entendeu mal o propósito principal do ViewModel. É uma questão de separação de preocupações. O ViewModel não deve manter referências a nenhuma visão, já que sua responsabilidade é manter os dados que estão sendo exibidos pela camada de visão. Os componentes da IU, também conhecidos como visualizações, são mantidos pela camada de visualização e o sistema Android recriará as visualizações, se necessário. Manter uma referência a visualizações antigas entrará em conflito com esse comportamento e causará vazamentos de memória.
Jackey
16

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.

humble_wolf
fonte
26
Por que a classe AndroidViewModel existe?
Alex Berdnikov
1
@AlexBerdnikov O objetivo do MVVM é isolar a visualização (Activity / Fragment) de ViewModel ainda mais do que MVP. Para que seja mais fácil testar.
hushed_voice
3
@free_style Obrigado pelo esclarecimento, mas a questão ainda permanece: se não devemos manter o contexto em ViewModel, por que a classe AndroidViewModel existe? Todo o seu propósito é fornecer contexto de aplicação, não é?
Alex Berdnikov
7
@AlexBerdnikov Usar o contexto Activity dentro do viewmodel pode causar vazamentos de memória. Portanto, ao usar a classe AndroidViewModel, você será fornecido pelo Contexto do aplicativo, que (com sorte) não estará causando nenhum vazamento de memória. Portanto, usar AndroidViewModel pode ser melhor do que passar o contexto da atividade para ele. Mas ainda assim, o teste será difícil. Esta é minha opinião sobre isso.
hushed_voice
1
Não consigo acessar o arquivo da pasta res / raw do repositório?
Fugogugo
15

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

Vincent Williams
fonte
1
Estou usando ResourcesProvider com Dagger no AppModule. Essa é uma boa abordagem para obter o contexto de ResourcesProvider ou AndroidViewModel é melhor obter contexto para recursos?
Usman Rana
@Vincent: Como usar resourceProvider para obter Drawable dentro de ViewModel?
Bulma
@Vegeta Você adicionaria um método como getDrawableRes(@DrawableRes int id)dentro da classe ResourceProvider
Vincent Williams
1
Isso vai contra a abordagem da Arquitetura Limpa, que afirma que as dependências da estrutura não devem cruzar os limites da lógica de domínio (ViewModels).
IgorGanapolsky
1
@IgorGanapolsky VMs não são exatamente lógicas de domínio. Lógica de domínio são outras classes, como interatores e repositórios, para citar alguns. As VMs se enquadram na categoria "cola", pois elas interagem com seu domínio, mas não diretamente. Se suas VMs fazem parte do seu domínio, você deve reconsiderar como está usando o padrão, pois está atribuindo a elas muita responsabilidade.
mradzinski,
9

TL; 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!

4gus71n
fonte
5
TL; DR deve estar no topo
Jacques Koorts
1
Obrigado pela sua resposta. No entanto, por que você usaria dagger para injetar o contexto se pudesse fazer seu viewmodel se estender do androidviewmodel e usar o contexto integrado que a própria classe fornece? Especialmente considerando a quantidade ridícula de código clichê para fazer o dagger e o MVVM funcionarem juntos, a outra solução parece muito mais clara. Quais são seus pensamentos sobre isso?
Josip Domazet
8

Como outros mencionaram, há o AndroidViewModelque você pode derivar para obter o aplicativo, Contextmas pelo que reuni nos comentários, você está tentando manipular @drawables de dentro do seu, o ViewModelque anula o propósito do MVVM.

Em geral, a necessidade de ter um Contextem seu ViewModelquase universalmente sugere que você deve considerar repensar como você divide a lógica entre seus Viewe ViewModels.

Em vez de ViewModelresolver 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 pelo ViewModel. Digamos que você precise que diferentes drawables sejam exibidos em uma visualização para o estado ativado / desativado - é o ViewModelque deve manter o estado (provavelmente booleano), mas é a Viewfunçã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 Enumvalor em R.drawable.*(por exemplo, naipes)

Ou talvez você precise do Contextpara algum componente que usa dentro do seu ViewModel- então, crie o componente fora do ViewModele passe-o. Você pode usar DI, ou singletons, ou criar o Contextcomponente -dependente antes de inicializar o ViewModelin Fragment/ Activity.

Por que se preocupar: Contexté uma coisa específica do Android, e depender daqueles em ViewModels é 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.

Ivan Bartsov
fonte
5

tem uma referência ao contexto do aplicativo, no entanto, contém o código específico do Android

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 ViewModelcomo faria normalmente e forneça a ele o ApplicationContext por meio de ViewModelProviders.Factory como faria normalmente.

EpicPandaForce
fonte
3

você pode acessar o contexto do aplicativo getApplication().getApplicationContext()de dentro do ViewModel. É disso que você precisa para acessar recursos, preferências, etc.

Alessandro Crugnola
fonte
Eu acho que para restringir minha pergunta. É ruim ter uma referência de contexto dentro do viewmodel (isso não afeta o teste?) E usar a classe AndroidViewModel afetaria Dagger de alguma forma? Não está vinculado ao ciclo de vida da atividade? Estou usando o Dagger para controlar o ciclo de vida dos componentes
Vincent Williams
14
A ViewModelclasse não possui o getApplicationmétodo.
beroal 01 de
4
Não, mas AndroidViewModelfaz
4Oh4
1
Mas você precisa passar a instância do Application em seu construtor, é o mesmo que acessar a instância do Application a partir dele
John Sardinha
2
Não é um grande problema ter o contexto do aplicativo. Você não quer ter um contexto de atividade / fragmento porque você está comprometido se o fragmento / atividade for destruído e o modelo de visualização ainda tiver uma referência ao contexto agora inexistente. Mas você nunca terá o contexto do APPLICATION destruído, mas a VM ainda tem uma referência a ele. Direito? Você pode imaginar um cenário em que seu aplicativo sai, mas o Viewmodel não? :)
user1713450
3

Você 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

Rohit Sharma
fonte
2
Esta é uma observação justa, mas algumas das bibliotecas de back-end ainda requerem contextos de aplicativos, como MediaStore. A resposta de 4gus71n abaixo explica como se comprometer.
Bryan W. Wagner
1
Sim, você pode usar o contexto do aplicativo, mas não o contexto das atividades, pois o contexto do aplicativo vive em todo o ciclo de vida do aplicativo, mas não o contexto da atividade, pois passar o contexto da atividade para qualquer processo assíncrono pode resultar em vazamentos de memória. O contexto mencionado em minha postagem é atividade Contexto. Mas você ainda deve tomar cuidado para não passar contexto para nenhum processo assíncrono, mesmo que seja o contexto de aplicativos.
Rohit Sharma de
2

Eu estava tendo problemas SharedPreferencesao usar a ViewModelclasse, então segui o conselho das respostas acima e fiz o seguinte usando AndroidViewModel. Tudo parece ótimo agora

Para 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;
    }
}
Davejoem
fonte
0

Eu criei desta forma:

@Module
public class ContextModule {

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

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:

@Inject
@Named("AppContext")
Context context;
Loopidio
fonte
0

Use o seguinte padrão:

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

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.

Hufman
fonte
E para habilitar o suporte de ligação de dados, basta adicionar um BindingAdapter simples como:@BindingAdapter("android:text") fun setText(view: TextView, value: Context.() -> String) { view.text = view.context.run(value) }
hufman