Skip to content

Android Chat Messenger with Firestore (MVVM Edition) with Dagger, LiveData, and Coroutines

Let’s be real. MVVM and Dagger are very important concepts in Android Development. Yet, nobody really knows what’s the ‘right’ way to implement either of them.

MVVM is simple enough, at least in its vanilla form. You’d simply follow this dependency diagram.

The View knows of only the model. The View Model knows of only the Model. The View Model doesn’t know the View and the Model doesn’t know the View Model. The View Model then takes Observable streams set up by the Model to pass data to the View.

That’s all very simple and you can see me doing that building an Instant Messenger with Pusher Chatkit. Chatkit is a specialised SDK for chats and instant messaging, but that’s not what we’re using today.

When you introduce Dagger into the mix, oh boy does it get complicated. First of all, Dagger’s documentation is notoriously shit. Not to mention, there’s about 3 different versions of Dagger which can all be used and abused in various ways in Android.

The official Android Developer site also has a tutorial on implementing MVVM with Dagger but even that doesn’t cover all the grounds.

What I’m getting at here is that there’s no definite ‘correct’ way to use MVVM with Dagger. Not yet at least. This will be my attempt to show you how I’ve done it through an MVVM version of my Firestore Chat App.

This app makes use of Dagger-Android, LiveData, Firestore, Firebase Auth, Coroutines, and the creation of Base activities. Not enough tutorials I’ve seen online cover base activities, as important as they are.

Yes I know Dagger-Android is officially deprecated and Dagger 2 is the way to go. Believe me, I wanted to do a pure Dagger 2 implementation at first as well, but I found Dagger-Android useful in the end.

This post will be focused on the implementation of MVVM and Dagger, rather than the concept of the chat application itself. If that’s what you’re after as well as the advantages of Firestore to Chatkit and such, see my post on the vanilla edition of this app.

Finally, before we get into it. This is by no means an introduction to any of the concepts and technologies featured in this article. If you are unfamiliar with MVVM, Dagger, or Firebase, you may have a hard time understanding. I recommend giving a good old Google search to start learning these techs. Or better yet, look for my own tutorials on them.

Dependencies

There’s quite a few dependencies here so I’ll let the code comments do the talking.

implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.fragment:fragment-ktx:1.1.0'
implementation 'androidx.preference:preference-ktx:1.1.0'

// LiveData & ViewModel
// Databinding and ViewModel
kapt 'com.android.databinding:compiler:3.1.4'
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services: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'

// Firebase
implementation 'com.google.firebase:firebase-core:16.0.4'
implementation 'com.google.firebase:firebase-auth:16.0.5'
implementation 'com.google.firebase:firebase-firestore:17.1.2'

The Dagger Setup

I won’t go over every single class, but rather one class in each folder just to give a general idea. For a full understanding, you can check out the source code at the bottom.

Scopes

The injection folder is where all the Dagger stuff’s at. 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.

@Scope
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class AppScoped

 

 

Modules

@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

    ....

5 module classes here, for binding and providing my Activities, Repositories, ViewModels, ViewStates (if you don’t know, I’ll explain further below), and the overall AppModule.

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.

ViewModel Key

@kotlin.annotation.Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

One class I couldn’t figure out a proper placement for is the ViewModel key. It’s an annotation class so I guess it would fit in with the scopes, but at the same time, it’s not really a scope.

App Component

@AppScoped
@Component(modules = [
    AppModule::class,
    ActivityBindingModule::class,
    AndroidSupportInjectionModule::class,
    ViewModelModule::class,
    RepositoriesModule::class,
    ViewStatesModule::class
])
interface AppComponent: AndroidInjector<Application> {

    @Component.Builder
    interface Builder {

        @BindsInstance
        fun application(application: Application): Builder

        fun build(): AppComponent
    }

}

We only have one component here and it’s the app component. Useful for bringing all the modules together. Then there is the provider folder but that’s optional.

It’s useful as it provides global app-wide access to resources we define in there, but it’s not crucial to the Dagger setup. Check the source code if it piques interest.

The MVVM Setup

Before we get into the real MVVM pieces, let’s cover a few classes that get the gears running.

Application Class

class Application: DaggerApplication() {

    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        return DaggerAppComponent.builder().application(this).build()
    }
}

Don’t forget to set this Application class in your manifest.

View Model Factory

@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 handled the creation of all our View Models.. as you’d expect from a factory class. We use this factory class in our View Model Module in the injection folder.

Now that’s all out of the way, let’s get to actual meat of our app.

The Model…. Repository….

class FirebaseRepository {

    val auth = FirebaseAuth.getInstance()
    val firestore = FirebaseFirestore.getInstance()

    var roomId = ""
    val chatMessagesLiveData = MutableLiveData<List<ChatMessage>>()

    fun user(): FirebaseUser? = auth.currentUser

    suspend fun login(email: String, password: String): AuthResult {
        return auth.signInWithEmailAndPassword(email, password).await()
    }

    suspend fun register(email: String, password: String): AuthResult {
        return auth.createUserWithEmailAndPassword(email, password).await()
    }

    fun observeChatMessages() {

        firestore.collection("rooms")
            .document(roomId)
            .collection("messages")
            .orderBy("timestamp")
            .addSnapshotListener { messagesSnapshot, exception ->

                if (exception != null) {
                    exception.printStackTrace()
                    return@addSnapshotListener
                }

                val messages = messagesSnapshot?.documents?.map {
                    ChatMessage(
                        it["text"] as String,
                        it["user"] as String,
                        (it["timestamp"]) as Date
                    )
                }

                messages?.let { chatMessagesLiveData.postValue(messages) }
            }
    }


}

Unlike most Retrofit-ran applications, I’m using Firebase Auth and Firestore as my data sources here. A retrofit would just switch these functions out for a service that executes API calls.

Where possible, we try to use coroutines (via suspend functions) as this results in our code being super concise and intuitive. We can call them like they’re synchronous code within our View Model.

This wasn’t always possible though, so I made use of LiveData from the Model as well. You’ll find that later, I gather all this data in the ViewModel as a single LiveData to emit to the View, staying true to our SSOT (Single-source-of-truth) principle.

This is currently the only Repository class in this application.

The Base Classes

ViewState
open class ViewState (
    var newActivity: KClass<*>? = null,
    var clearActivityOnIntent: Boolean = false,
    var intentExtras: Bundle = Bundle()
)

This ViewState here is not something you’ll see in many other applications or implementations of MVVM, but it is nonetheless a very important component here.

This class (to be extended by more specialised ViewState implementations per activity) is to contain a representation of the UI in a single object. This is the single LiveData that the ViewModel will emit. This lets us stick true to the single-source-of-truth principle.

BaseViewModel
abstract class BaseViewModel<S: ViewState>(
    val firebaseRepository: FirebaseRepository,
    var viewState: S): ViewModel() {

    protected val stateLiveData = MutableLiveData<ViewState>()

    private val networkJob = Job()
    protected val networkScope = CoroutineScope(Dispatchers.IO + networkJob)

    open fun checkUserLoggedIn() {

        if (firebaseRepository.user() == null) {
            viewState.newActivity = LoginActivity::class
            viewState.clearActivityOnIntent = true
            updateUi()
        }
    }

    fun handleSignOut() {
        firebaseRepository.auth.signOut()
        checkUserLoggedIn()
    }

    fun getState(): MutableLiveData<ViewState> {
        return this.stateLiveData
    }

    fun resetNewActivity() {
        viewState.newActivity = null
        updateUi()
    }

    fun updateUi() {
        stateLiveData.postValue(viewState)
    }

}

The BaseViewModel contains our coroutine scopes, LiveData for our ViewState, and some common functions used in many of its implementations. We use generic typing too to make it easier to implement with different ViewState classes.

You might wonder what resetNewActivity() and updateUi() are really there for. These functions are there to simplify the way we write our implementations which you’ll see further down.

BaseFragment
abstract class BaseFragment<V: BaseViewModel<S>, S: ViewState>(open var viewModel: V): Fragment() {

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    protected open fun onBindViewModel() {
        observeState()
        viewModel.checkUserLoggedIn()
    }

    protected open fun onUnbindViewModel() {
        // Empty lifecycle function to be overridden
    }

    abstract fun updateUi(state: S)

    abstract fun attachClickListeners()

    abstract fun getLayoutResourceFile(): Int

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {

        return inflater.inflate(getLayoutResourceFile(), container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        attachClickListeners()
    }

    override fun onResume() {
        super.onResume()
        onBindViewModel()
    }

    override fun onPause() {
        super.onPause()
        onUnbindViewModel()
    }

    private fun observeState() {
        viewModel.getState().observe(this, Observer { state -> updateUi(state as S) })
    }

    protected fun KClass<*>.start(clearingLast: Boolean) {
        val intent = Intent(activity, this.java)
        startActivity(intent)

        viewModel.resetNewActivity()
        if (clearingLast) activity?.finish()
    }

    protected fun KClass<*>.start(clearingLast: Boolean, extras: Bundle) {
        val intent = Intent(activity, this.java)
        intent.putExtras(extras)
        startActivity(intent)

        viewModel.resetNewActivity()
        if (clearingLast) activity?.finish()
    }

}

Finally, we have the Base View in the form of a Fragment.

onBindViewModel() and onUnbindViewModel() act as lifecycle events which are open to allow them to be overriden by its implementations.

This base class initiates observing the LiveData that will pass our ViewState so we don’t have to implement this in the ViewModels of each activity. All we do in those classes is edit the ViewState they contain and call updateUI().

Finally, you might notice the extension function KClass<*>.start(). If you stick true to MVVM principles, you’ll find cases where you have to click a button to start an activity and suddenly, you run into a few problems. The ViewModel must handle the logic of that button, but the ViewModel must also not contain any Android classes or edit the UI directly in any way.

The Base ViewState has a newActivity field which is also of type KClass. In our implementations, we’ll look for changes in newActivity and if it’s not null, we’ll call start() on that activity.

The MVVM Implementation

Though we have a few activities in the app, we’ll only cover one of them: LobbyActivity.

LobbyActivity
class LobbyActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var lobbyFragment: LobbyFragment

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_base)

        var fragment = supportFragmentManager.findFragmentById(R.id.contentFrame)

        if (fragment == null) {
            fragment = lobbyFragment
            ActivityUtils.addFragmentToActivity(
                supportFragmentManager,
                fragment,
                R.id.contentFrame
            )
        }
    }

}

The activity class only has one purpose: to initialise the fragment. Occasionally, it might have to implement lifecycle methods like onBackPressed() which can’t be handled by the fragment without adding interfaces to the fragment. This is however, not very ideal.

LobbyViewState
class LobbyViewState(
    var errorMessage: String = "",
    var enterBtnEnabled: Boolean = true,
    var clearRoomTextfield: Boolean = false
): ViewState()

LobbyViewState extends the original ViewState class. On top of having access to the fields declared in ViewState, it also contains fields that represent the UI specific to the LobbyActivity.

LobbyViewModel
@AppScoped
class LobbyViewModel @Inject constructor(
    firebaseRepository: FirebaseRepository,
    viewState: LobbyViewState
): BaseViewModel<LobbyViewState>(firebaseRepository, viewState) {

    fun handleEnterButton(roomId: String) {

        if (!validate(roomId)) {
            updateUi()
            return
        }

        viewState.enterBtnEnabled = false
        updateUi()

        val errorHandler = CoroutineExceptionHandler { _, exception ->

            exception.printStackTrace()
            viewState.errorMessage = "Something went wrong. Please try again"
            viewState.enterBtnEnabled = true
            updateUi()
        }

        networkScope.launch(errorHandler) {

            firebaseRepository.addUserToRoom(roomId)
            launchRoomActivity(roomId)
        }

    }

    private fun launchRoomActivity(roomId: String) {
        viewState.enterBtnEnabled = true
        viewState.clearRoomTextfield = true
        viewState.newActivity = RoomActivity::class
        viewState.clearActivityOnIntent = false
        viewState.intentExtras.putString(AppConstants.INTENT_EXTRA_ROOMID, roomId)
        updateUi()
    }

    private fun validate(roomId: String): Boolean {

        if (roomId.isEmpty()) {
            viewState.errorMessage = "Please enter a Room ID"
            return false
        }

        viewState.errorMessage = ""
        return true
    }

    fun checkRoomIdPreference(roomId: String?) {
        roomId?.let { launchRoomActivity(it) }
    }


}

As you can see, this implementation of ViewModel doesn’t contain any LiveData, declarations of Coroutine Scopes, or any of the boilerplate needed to get our data running. It is almost pure logic, receiving data from the repository, and setting up the data to pass to the View.

This is because all that setup is hiding in the BaseViewModel, so each implementation of our ViewModel only has to concern itself with editing the fields in its ViewState and calling updateUi(). This then triggers from the BaseViewModel to pass the ViewState as LiveData which is being observed by the Fragment.

LobbyFragment
@ActivityScoped
class LobbyFragment @Inject constructor(
    override var viewModel: LobbyViewModel
): BaseFragment<LobbyViewModel, LobbyViewState>(viewModel)
{

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        this.viewModel = ViewModelProviders.of(this, viewModelFactory).get(LobbyViewModel::class.java)

        setHasOptionsMenu(true)
    }

    override fun onBindViewModel() {
        super.onBindViewModel()
        checkRoomIdPreference()
    }

    private fun checkRoomIdPreference() {
        val preferences = PreferenceManager
            .getDefaultSharedPreferences(activity)

        val roomId = preferences.getString(AppConstants.PREFERENCE_ROOMID, null)
        viewModel.checkRoomIdPreference(roomId)
    }

    override fun getLayoutResourceFile(): Int {
        return R.layout.fragment_lobby
    }

    override fun updateUi(state: LobbyViewState) {

        button_enter.isEnabled = state.enterBtnEnabled
        textview_error_enter.text = state.errorMessage
        state.newActivity?.start(state.clearActivityOnIntent, state.intentExtras)

        if (state.clearRoomTextfield) {
            state.clearRoomTextfield = false
            edittext_roomid.setText("")
        }
    }

    override fun attachClickListeners() {
        button_enter.setOnClickListener { viewModel.handleEnterButton(edittext_roomid.text.toString()) }
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.menu_main, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when(item.itemId) {

            R.id.nav_signout -> {
                viewModel.handleSignOut()
                true
            }
            else -> false
        }
    }

}

Speaking of our fragment, there’s a bit more going on as it implements Android lifecycle methods. It also instantiates the ViewModel that goes with it.

It overrides updateUi to make the changes to the UI based on the data contained in the ViewState. As I mentioned earlier, it also starts any new activity if it finds that this field in the ViewState is not null.

Again, observing the LiveData and such is handled within BaseView, so we don’t need to implement that here. When the LiveData is updated, the BaseFragment calls updateUi() which makes it easy for us to handle here.

Get the Source Code

I can’t fit the whole codebase in one article. The easiest way to get a grasp of the whole structure is to view the code in whole and play around with it. Find it here on Github.

I hope you find this article useful. Once again, this is my way of implementing MVVM with Dagger, Coroutines, and Firestore. This isn’t necessarily the ‘right’ way. Each developer has their own way and I find that this is what works best for me.