117
votes

I am trying to implement MVVM pattern in my android app. I have read that ViewModels should contain no android specific code (to make testing easier), however I need to use context for various things (getting resources from xml, initializing preferences, etc). What is the best way to do this? I saw that AndroidViewModel has a reference to the application context, however that contains android specific code so I'm not sure if that should be in the ViewModel. Also those tie into the Activity lifecycle events, but I am using dagger to manage the scope of components so I'm not sure how that would affect it. I am new to the MVVM pattern and Dagger so any help is appreciated!

15
Just in case someone is trying to use AndroidViewModel but getting Cannot create instance exception then you can refer to my this answer stackoverflow.com/a/62626408/1055241gprathour
You shouldn't use Context in a ViewModel, create a UseCase instead to get the Context from that wayRuben Caster
@RubenCaster do you have any sample or GitHub link for that?Parmesh
@Parmesh No, sorry. Its a private project =(Ruben Caster

15 Answers

101
votes

You can use an Application context which is provided by the AndroidViewModel, you should extend AndroidViewModel which is simply a ViewModel that includes an Application reference.

79
votes

For Android Architecture Components View Model,

It's not a good practice to pass your Activity Context to the Activity's ViewModel as its a memory leak.

Hence to get the context in your ViewModel, the ViewModel class should extend the Android View Model Class. That way you can get the context as shown in the example code below.

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

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

    //... ViewModel methods 

}
57
votes

It's not that ViewModels shouldn't contain Android specific code to make testing easier, since it's the abstraction that makes testing easier.

The reason why ViewModels shouldn't contain an instance of Context or anything like Views or other objects that hold onto a Context is because it has a separate lifecycle than Activities and Fragments.

What I mean by this is, let's say you do a rotation change on your app. This causes your Activity and Fragment to destroy itself so it recreates itself. ViewModel is meant to persist during this state, so there's chances of crashes and other exceptions happening if it's still holding a View or Context to the destroyed Activity.

As for how you should do what you want to do, MVVM and ViewModel works really well with the Databinding component of JetPack. For most things you would typically store a String, int, or etc for, you can use Databinding to make the Views display it directly, thus not needing to store the value inside ViewModel.

But if you don't want Databinding, you can still pass the Context inside the constructor or methods to access the Resources. Just don't hold an instance of that Context inside your ViewModel.

20
votes

Short answer - Don't do this

Why ?

It defeats the entire purpose of view models

Almost everything you can do in view model can be done in activity/fragment by using LiveData instances and various other recommended approaches.

16
votes

What I ended up doing instead of having a Context directly in the ViewModel, I made provider classes such as ResourceProvider that would give me the resources I need, and I had those provider classes injected into my ViewModel

14
votes

As others have mentioned, there's AndroidViewModel which you can derive from to get the app Context but from what I gather in the comments, you're trying to manipulate @drawables from within your ViewModel which defeats the purpose MVVM.

In general, the need to have a Context in your ViewModel almost universally suggests you should consider rethinking how you divide the logic between your Views and ViewModels.

Instead of having ViewModel resolve drawables and feed them to the Activity/Fragment, consider having the Fragment/Activity juggle the drawables based on data possessed by the ViewModel. Say, you need different drawables to be displayed in a view for on/off state -- it's the ViewModel that should hold the (probably boolean) state but it's the View's business to select the drawable accordingly.

DataBinding makes it quite easy:

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

If you have more states and drawables, to avoid unwieldy logic in the layout file you can write a custom BindingAdapter that translates, say, an Enum value into an R.drawable.* ref, e.g.:

enum class CatType { NYAN, GRUMPY, LOL }

class CatViewModel {
    val catType: LiveData<CatType> = ...
// View-tier logic, takes the burden of knowing
// Contexts and R.** refs from the ViewModel
@BindingAdapter("bindCatImage")
fun bindCatImage(view: ImageView, catType: CatType) = view.apply {
    val resource = when (value) {
        CatType.NYAN -> R.drawable.cat_nyan
        CatType.GRUMPY -> R.drawable.cat_grumpy
        CatType.LOL -> R.drawable.cat_lol
    }
    setImageResource(resource)
}
<ImageView
    bindCatType="@{vm.catType}"
    ... />

If you need the Context for some component that you use within your ViewModel -- then, create the component outside the ViewModel and pass it in. You can use DI, or singletons, or create the Context-dependent component right before initialising the ViewModel in Fragment/Activity.

Why bother

Context is an Android-specific thing, and depending on it in ViewModels is unwieldy for unit tests (of course you can use AndroidJunitRunner for android-specific stuff, but it just makes sense to have cleaner code without the extra dependency). If you don't depend on Context, mocking everything for the ViewModel test is easier. So, rule of thumb is: don't use Context in ViewModels unless you have a very good reason to do so.

9
votes

TL;DR: Inject the Application's context through Dagger in your ViewModels and use it to load the resources. If you need to load images, pass the View instance through arguments from the Databinding methods and use that View context.

The MVVM is a good architecture and It's definitely the future of Android development, but there's a couple of things that are still green. Take for example the layer communication in a MVVM architecture, I've seen different developers (very well known developers) use LiveData to communicate the different layers in different ways. Some of them use LiveData to communicate the ViewModel with the UI, but then they use callback interfaces to communicate with the Repositories, or they have Interactors/UseCases and they use LiveData to communicate with them. Point here, is that not everything is 100% define yet.

That being said, my approach with your specific problem is having an Application's context available through DI to use in my ViewModels to get things like String from my strings.xml

If I'm dealing with image loading, I try to pass through the View objects from the Databinding adapter methods and use the View's context to load the images. Why? because some technologies (for example Glide) can run into issues if you use the Application's context to load images.

Hope it helps!

5
votes

has a reference to the application context, however that contains android specific code

Good news, you can use Mockito.mock(Context.class) and make the context return whatever you want in tests!

So just use a ViewModel as you normally would, and give it the ApplicationContext via the ViewModelProviders.Factory as you normally would.

3
votes

you can access the application context from getApplication().getApplicationContext() from within the ViewModel. This is what you need to access resources, preferences, etc..

3
votes

You should not use Android related objects in your ViewModel as the motive of using a ViewModel is to separate the java code and the Android code so that you can test your business logic separately and you will have a separate layer of Android components and your business logic and data ,You should not have context in your ViewModel as it may lead to crashes

3
votes

I was having trouble getting SharedPreferences when using the ViewModel class so I took the advice from answers above and did the following using AndroidViewModel. Everything looks great now

For the 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>"));
    }

}

And in the 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;
    }
}
2
votes

Using Hilt

@Module
@InstallIn(SingletonComponent::class)
class AppModule {

    @Singleton
    @Provides
    fun provideContext(application: Application): Context = application.applicationContext
}

Then pass it via constructor

class MyRepository @Inject constructor(private val context: Context) {
...
}
1
votes

Use the following pattern:

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

The problem with injecting a Context into the ViewModel is that the Context can change at any time, depending on screen rotation, night mode, or system language, and any returned resources can change accordingly. Returning a simple resource ID causes problems for extra parameters, like getString substitutions. Returning a high-level result and moving rendering logic to the Activity makes it harder to test.

My solution is to have the ViewModel generate and return a function that is later run through the Activity's Context. Kotlin's syntactic sugar makes this incredibly easy!

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)
  }
}

This allows ViewModel to hold all of the logic for calculating the displayed information, verified by unit tests, with the Activity being a very simple representation with no internal logic to hide bugs.

-1
votes

I created it this way:

@Module
public class ContextModule {

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

And then I just added in AppComponent the ContextModule.class:

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

And then I injected the context in my ViewModel:

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