Skip to content

Android MVVM, Dagger 2 and Android Architecture Components

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.

Check it out here on Github.

And as always, happy coding ༼ つ ◕_◕ ༽つ