The war between MVP and MVVM has been long settled. With the release of Android Architecture Components, there’s no doubt that MVVM is the definite way to go now.
If you don’t know what MVVM is, I recommend checking this out first.
In a pure sense, MVVM isn’t so hard to implement. Use the components and ensure the relations between them are proper. The View only knows of the View Model, and the View Model only knows of the Model. Simple.
Bring in Dependency Injection into the mix and suddenly, things get a lot harder. The Android Dev site has its own guide to implementing MVVM with Dagger but I personally didn’t find it too useful in terms of actually coding the architecture.
Thus, the community have tried all sorts of ways to implement MVVM with Dagger. This is my way of doing that.
By no means am I a dagger expert. My reasoning for some of the components used might be off, but I chose this app structure because it is what made the most sense to me.
Import the Dependencies
Before we begin, make sure you have Dagger loaded in your app/build.gradle
file, as well as ViewModel, binding, and core Kotlin libraries.
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' implementation 'androidx.core:core-ktx:1.1.0' implementation 'androidx.fragment:fragment-ktx:1.1.0' // Databinding and ViewModel kapt 'com.android.databinding:compiler:3.1.4' implementation "android.arch.lifecycle:extensions:1.1.1" // Dagger implementation 'com.google.dagger:dagger-android:2.20' implementation 'com.google.dagger:dagger-android-support:2.20' kapt 'com.google.dagger:dagger-android-processor:2.20' kapt 'com.google.dagger:dagger-compiler:2.20'
Don’t forget the apply plugins on top of the file as well.
App Structure
3 main folders here, entity, injection, and mvvm. Entity holds all our data classes. Injection handles everything with Dagger. Mvvm holds our actual app elements.
Setting up the Injection folder
Scopes
@Scope @kotlin.annotation.Retention(AnnotationRetention.RUNTIME) annotation class ActivityScoped
@Scope @kotlin.annotation.Retention(AnnotationRetention.RUNTIME) annotation class AppScoped
@Scope @kotlin.annotation.Retention(AnnotationRetention.RUNTIME) @kotlin.annotation.Target( AnnotationTarget.TYPE, AnnotationTarget.FUNCTION, AnnotationTarget.CLASS ) annotation class FragmentScoped
We have 3 scope annotation classes, ActivityScoped, AppScoped, and FragmentScoped. This allows us to 1) preserve the object instance and provide it as a local singleton for the duration of the scoped component and 2) satisfy Dagger’s requirement for scoped components.
Modules
@Module abstract class AppModule { @Binds abstract fun bindContext(application: Application): Context }
@Module abstract class ActivityBindingModule { @ActivityScoped @ContributesAndroidInjector(modules = [LoginModule::class]) internal abstract fun loginActivity(): LoginActivity @ActivityScoped @ContributesAndroidInjector(modules = [RegisterModule::class]) internal abstract fun registerActivity(): RegisterActivity }
@Module abstract class ViewModelModule { @Binds @AppScoped abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory @Binds @IntoMap @ViewModelKey(LoginViewModel::class) abstract fun bindLoginViewModel(viewModel: LoginViewModel): ViewModel @Binds @IntoMap @ViewModelKey(RegisterViewModel::class) abstract fun bindRegisterViewModel(viewModel: RegisterViewModel): ViewModel }
@Module class FirebaseRepositoryModule { @Provides @AppScoped fun provideFirebaseRepository(): FirebaseRepository = FirebaseRepository() }
4 module classes here. One module for handling activity binding, another for binding ViewModels, then we have our App and Repository modules. I’m using Firebase to fetch my data in this app, but this repository can be any class that fetches data, like from Retrofit for example.
Now, you’ll be getting a lot of errors because we haven’t defined any of these ViewModels, Activities, or Repositories yet. Don’t worry, we’ll get there.
App Component
@AppScoped @Component(modules = [ AppModule::class, ActivityBindingModule::class, AndroidSupportInjectionModule::class, ViewModelModule::class, FirebaseRepositoryModule::class ]) interface AppComponent: AndroidInjector<Application> { @Component.Builder interface Builder { @BindsInstance fun application(application: Application): Builder fun build(): AppComponent } }
We only need one component class which is the AppComponent. It loads all our previous modules, as well as the AndroidSupportInjectionModule which we can simply import. This module helps with binding our fragments.
Application Class
class Application: DaggerApplication() { override fun applicationInjector(): AndroidInjector<out DaggerApplication> { return DaggerAppComponent.builder().application(this).build() } }
Now that we have our app component, we can inject it in our Application class. Make sure to declare this application class in your manifest.
Creating the MVVM Folder
Now that our injection is set up, we can move on to our MVVM folder.
ViewModelFactory
Before we create our actual MVVM activities, we have to set up this ViewModelFactory first.
@AppScoped public class ViewModelFactory implements ViewModelProvider.Factory { private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators; @Inject public ViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) { this.creators = creators; } @SuppressWarnings("unchecked") @Override public <T extends ViewModel> T create(Class<T> modelClass) { Provider<? extends ViewModel> creator = creators.get(modelClass); if (creator == null) { for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) { if (modelClass.isAssignableFrom(entry.getKey())) { creator = entry.getValue(); break; } } } if (creator == null) { throw new IllegalArgumentException("unknown model class " + modelClass); } try { return (T) creator.get(); } catch (Exception e) { throw new RuntimeException(e); } } }
This class is responsible for instantiating our ViewModels.
Creating our Activities
class LoginActivity: DaggerAppCompatActivity() { @Inject lateinit var loginFragment: LoginFragment override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) var fragment = supportFragmentManager.findFragmentById(R.id.contentFrame) if (fragment == null) { fragment = loginFragment ActivityUtils.addFragmentToActivity( supportFragmentManager, fragment, R.id.contentFrame ) } } }
@ActivityScoped class LoginFragment @Inject constructor(): Fragment(), BaseView { private lateinit var viewModel: LoginViewModel @Inject lateinit var viewModelFactory: ViewModelProvider.Factory override fun bindViewModel() { checkUserLoggedIn() observeNewActivity() } override fun unbindViewModel() { } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel = ViewModelProviders.of(this, viewModelFactory).get(LoginViewModel::class.java) attachClickListeners() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return inflater.inflate(R.layout.fragment_login, container, false) } override fun onResume() { super.onResume() bindViewModel() } override fun onPause() { super.onPause() unbindViewModel() } private fun observeNewActivity() { viewModel.activityToStart.observe(this, Observer { activityToStart -> activityToStart?.let { viewModel.resetActivityToStart() startActivity(Intent(activity, activityToStart.java)) } }) } private fun attachClickListeners() { textview_register.setOnClickListener { viewModel.handleRegisterTextviewClicked() } } }
@AppScoped class LoginViewModel @Inject constructor(var firebaseRepository: FirebaseRepository): ViewModel() { val activityToStart = MutableLiveData<KClass<*>>() fun getUser(): FirebaseUser? = firebaseRepository.user() fun handleRegisterTextviewClicked() { activityToStart.postValue(RegisterActivity::class) } fun resetActivityToStart() { activityToStart.postValue(null) } }
@Module(includes = [LoginModule.LoginAbstractModule::class]) class LoginModule { @ActivityScoped @Provides internal fun provideResourceProvider(context: LoginActivity): BaseResourceProvider { return ResourceProvider(context) } @Module interface LoginAbstractModule { @FragmentScoped @ContributesAndroidInjector fun loginFragment(): LoginFragment } }
Finally, the meat of our application. Each activity has 4 parts: Activity, Fragment, ViewModel, and Module.
Module injection of our Fragment, as well as any other classes we’d like to inject into the Fragment. The Activity loads the Fragment. The Fragment acts as our View in the MVVM sense. And of course, the ViewModel handles all the logic and communication between the View and the Model.
To inject our Repository (or our Model) into the ViewModel, we use a simple constructor injection.
Repository (Model)
class FirebaseRepository { // Put all your data sources here, LiveData and stuff }
The repository class we’re making is very simple. No injection, no inheritance. Just a plain class where we can share our data sources and LiveData, Observables, or whatever you want to use.
Get the Source Code
The code used in its article is all the set up for the MVVM edition of my FirestoreSmartChat app. At the time of this writing, it’s just a skeleton with all the dependency injection set up.
And as always, happy coding ༼ つ ◕_◕ ༽つ