Android MVVM — good practices.
This post is not tutorial about implementing MVVM architectural pattern —it is list of good practices and misconceptions of MVVM commonly committed by android developers.
ViewModel vs AndroidViewModel
tl;dr Do not use AndroidViewModel, it ruins whole testability which MVVM provides in the first place.
/* For simplification of example, this part assumes we don’t use UseCase layer (which should be used) */
AndroidViewModel is what its name says — it is plain ViewModel with addition of a little bit of Android — in constructor We need to pass Application instance and we can access it from within ViewModel. Thats all from differences. Just that, one simple thing to prevent you from mocking your ViewModel easily. It was said not a single time, no matter which architectural pattern you use, do not use android dependencies in Bussiness Logic Layer.
Ok, so what alternative do We have?- Creating wrapper classes which will be responsible for things you used, for either Application or any other android dependency. This way you will have exposed some specific functionalities which are actually being used and only these will be mocked in your Unit Tests— imagine mocking Application.getSharedPreferences()
or all SharedPreferences.get/put
methods — too much work.
Lets assume you used Application for getting some data from SharedPreferences
— what should be done is creating some Settings
class which will expose specific SharedPreferences
fields, and SharedPreferences
instance should be one of constructor parameters of Settings
class.
private const KEY_DARK_MODE = "dark_mode"class Settings(private val _sharedPreferences: SharedPreferences) {
var isDarkMode: Boolean
set (value) {
_sharedPreferences.edit {
putBoolean(KEY_DARK_MODE, value)
}
}
get() = _sharedPreferences.getBoolean(KEY_DARK_MODE)
}
What is more, this solution allows you setting and getting data from your prefs with simple kotlin syntax — _settings.isDarkMode = true
ViewModel instantiation
tl;dr Do not instantiate ViewModel
by constructor directly in your View, instead use ViewModelProvider
, and ViewModelFactory
, otherwise it will be bound to lifecycle and instantiated each time onCreate
is being called.
Each ViewModel
needs to have ViewModelFactory
, which will be responsible for instantiating in provider, here is example:
class LoginViewModelFactory(private val _settings: Settings): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return LoginViewModel(_settings)
}
}
ViewModelProvider
And to fetch it you use ViewModelProvider
, first parameter of ViewModelProvider
constructor is ViewModelStoreOwner
so in Activities and Fragments we can just pass this
, second parameter is ViewModelProvider.Factory
instance. On instantiated ViewModelProvider
you can invoke get
to acquire ViewModel of specified Class
type as parameter,
viewModel = ViewModelProvider(this, loginViewModelFactory)
.get(LoginViewModel::class.java)
ViewModelStoreOwner
ViewModelStoreOwner
takes care of proper instantiation of ViewModelStore
to keep it separated from configuration changes and app lifecycle, so data is being kept, and ViewModels are not reinstantiated. ViewModelStore
(and ViewModels) are being cleared only in onDestroy
when it is not caused by configuration changes (for example orientation change) - this is the magic of MVVM lifecycle independence, below is code from ComponentActivity
:
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
Ok, so we have ViewModelStoreOwner
in ViewModelProvider
, what’s next?
Long story short, get
method takes ViewModel instance from store if already instantiated, if it is not it is being instantiated and put into ViewModelStore
so when we request for ViewModel of this type after configuration change we will get the same instance, so already loaded data will be kept and won’t be fetched again.
/* There are ways to make it more abstract but it depends on which Dependency Injection library you use, shortly: THIS is tutorial making it with dagger2, and in koin you just provide it with method viewModel
inside your module — instantiating by constructor, all factories and providing are done by koin. */
No UI in ViewModel
tl;dr Do not define texts, images etc. in ViewModel — define it in View depending on current LiveData
value.
It somehow relates to AndroidViewModel
part — No android dependencies in bussiness logic. Acquiring strings or drawables in ViewModel requires context, which shouldn’t be included in your ViewModel as it is Android Dependency. Okay it can be done with some wrapper, but in this case if you set some String or Drawable
as LiveData
value it ruins testability.
Lets say you have some ImageView
and depending on the state it should display different image: for failure — red “X”, for loading — circular progress, for success — green ✓. If you fetched Drawable
from some wrapper you would have android dependency in ViewModel (Drawable
itself) and it would be hard to test, you would need to create Drawable
instance, and it is abstract class, so you would need to find some subclass for example GradientDrawable
, instantiate it, and then compare returned instance with your expected instance, but how would you differ failed, loading and success drawable?
Maybe this is not so accurate with strings but there is another reason - View must be separated from bussiness logic. Bussiness logic should tell you what is the state, and the view should react adequately to current state and this reaction should be defined in View layer.
So what is the solution? — Enum classes. It will be much better to just create enum with three states FAILED, IN_PROGRESS, SUCCESS, and then easily set your image in observer. This way you have strictly separated View and Logic layer.
viewModel.currentState.observe(this, Observer { state ->
val drawableId = when(state) {
DataState.FAILED -> R.drawable.failedIcon
DataState.IN_PROGRESS -> R.drawable.circularProgress
DataState.SUCCESS -> R.drawable.successIcon
}
stateImageView.setDrawable(drawableId)
}