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 Answers
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
}
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.
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 @drawable
s 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 View
s 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 ViewModel
s 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.
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!
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.
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
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;
}
}
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.
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;
AndroidViewModel
but gettingCannot create instance exception
then you can refer to my this answer stackoverflow.com/a/62626408/1055241 – gprathour