Clean and loosely coupled MVVM architecture in Android (Kotlin)

Anant Raman
11 min readJun 12, 2020

In the beginning, when I started exploring MVVM architecture I didn’t exactly find what I needed. Some of the illustrations were either too complicated or too basic. So this is an attempt of creating something just appropriate for the purpose.

THIS PROJECT IS ALSO AVAILABLE ON GITHUB. THE GITHUB LINK IS MENTIONED AT THE BOTTOM OF THIS ARTICLE.

NOTE: The entire project is available on Github.

This project is pretty simple with just a feature to sign in and sign up using Google firebase Authentication.

We won’t focus much on how to connect our application with firebase. You can go to the following link for the detailed documentation.
https://firebase.google.com/docs/android/setup

This is how the login and signup screen looks like.

Sign Up Screen
Login Screen

Let’s have a look at the XML of the two layouts.

Login layout :

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
>

<data>

<variable
name="viewmodel"
type="com.example.authenticationpoc.auth.login.viewmodel.LoginViewModel"
/>
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".auth.login.activities.LoginActivity"
>

<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:titleMarginStart="@dimen/dp_20"
app:titleTextColor="@color/secondaryText"
/>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/email_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_40"
android:layout_marginTop="@dimen/dp_180"
android:layout_marginEnd="@dimen/dp_40"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar_login"
>

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/txt_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/email"
android:textSize="@dimen/sp_16"
/>

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_40"
android:layout_marginTop="@dimen/dp_20"
android:layout_marginEnd="@dimen/dp_40"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/email_layout"
>

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/txt_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
android:inputType="textPassword"
android:textSize="@dimen/sp_16"
/>

</com.google.android.material.textfield.TextInputLayout>

<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_40"
android:layout_marginTop="@dimen/dp_20"
android:layout_marginEnd="@dimen/dp_40"
android:backgroundTint="@color/colorAccent"
android:padding="@dimen/dp_10"
android:text="@string/login"
android:textColor="@color/secondaryText"
app:layout_constraintTop_toBottomOf="@id/password_layout"
/>

<ProgressBar
android:id="@+id/login_prog_bar"
android:layout_width="@dimen/dp_60"
android:layout_height="@dimen/dp_60"
app:layout_constraintTop_toBottomOf="@id/btn_login"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="@dimen/dp_60"
android:layout_marginStart="@dimen/dp_160"
android:visibility="invisible"
/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dp_36"
android:gravity="center_horizontal"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/don_t_have_an_account_click_here_to"
android:textSize="@dimen/sp_16"
/>

<TextView
android:id="@+id/txt_signup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_2"
android:clickable="true"
android:focusable="true"
android:text="@string/sign_up"
android:textSize="@dimen/sp_16"
android:textStyle="bold"
/>

</LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

</layout>

Signup layout :

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
>

<data>

<variable
name="viewmodel"
type="com.example.authenticationpoc.auth.signup.viewmodel.SignUpViewModel"
/>
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/root_signup_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".signup.view.SignUpActivity"
>

<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_signup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:theme="@style/ToolbarColoredWhiteArrow"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:titleMarginStart="@dimen/dp_20"
app:titleTextColor="@color/secondaryText"
/>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/name_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_40"
android:layout_marginTop="@dimen/dp_100"
android:layout_marginEnd="@dimen/dp_40"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
>

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/txt_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/name"
android:textSize="@dimen/sp_16"
/>

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/email_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_40"
android:layout_marginTop="@dimen/dp_20"
android:layout_marginEnd="@dimen/dp_40"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/name_layout"
>

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/txt_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/email"
android:inputType="textEmailAddress"
android:textSize="@dimen/sp_16"
/>

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_40"
android:layout_marginTop="@dimen/dp_20"
android:layout_marginEnd="@dimen/dp_40"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/email_layout"
>

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/txt_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
android:inputType="textPassword"
android:textSize="@dimen/sp_16"
/>

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/confirm_password_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_40"
android:layout_marginTop="@dimen/dp_20"
android:layout_marginEnd="@dimen/dp_40"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/password_layout"
>

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/txt_confirm_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/confirm_password"
android:inputType="textPassword"
android:textSize="@dimen/sp_16"
/>

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/contact_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_40"
android:layout_marginTop="@dimen/dp_20"
android:layout_marginEnd="@dimen/dp_40"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/confirm_password_layout"
>

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/txt_contact"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/contact_number"
android:inputType="number"
android:textSize="@dimen/sp_16"
/>

</com.google.android.material.textfield.TextInputLayout>

<Button
android:id="@+id/btn_signup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_40"
android:layout_marginTop="@dimen/dp_20"
android:layout_marginEnd="@dimen/dp_40"
android:backgroundTint="@color/colorAccent"
android:padding="@dimen/dp_10"
android:text="@string/sign_up"
android:textColor="@color/secondaryText"
app:layout_constraintTop_toBottomOf="@id/contact_layout"
/>

<ProgressBar
android:id="@+id/signup_prog_bar"
android:layout_width="@dimen/dp_60"
android:layout_height="@dimen/dp_60"
android:layout_marginStart="@dimen/dp_160"
android:layout_marginTop="@dimen/dp_40"
android:visibility="invisible"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_signup"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

</layout>

Let’s add the following dependencies to our App level build.gradle

The versions for the dependencies are the latest to the day when the code was written. Make sure to add everything else to the Gradle as mentioned in the above firebase documentation link.
Also add these two plugins for google services and Kotlin respectively in your App level build.gradle

apply plugin: ‘com.google.gms.google-services’

apply plugin: ‘kotlin-kapt’

Add your google-services.json generated by following the above-mentioned process. Once you are ready with the project setup, we are good to go.

This is our folder structure for the project.

Files in expanded form :

Let’s create a directory with the name ‘core’. It has a generic response listener interface that will listen to the response received from firebase or any other external sources (eg API).

It is a good practice to remove all hardcoded data from our project and keep it either as constant or into the strings resources.

NOTE: Never keep the strings that you are going to use as a key for firebase document, collection, or field names in string resource files. Instead, keep them as constants.

We have a data helper class to remove the dependency of our repository from the view model. We will later see how we can use this DataHelper class to get the instance of our repository in our view model without calling it directly. This makes our code loosely coupled.

package com.example.authenticationpoc.core

import com.example.authenticationpoc.auth.login.repository.LoginRepository
import com.example.authenticationpoc.auth.login.repository.LoginUsingFirebase
import com.example.authenticationpoc.auth.signup.repository.SignUpRepository
import com.example.authenticationpoc.auth.signup.repository.SignUpUsingFirebase
import com.example.authenticationpoc.main.repository.HomeRepository
import com.example.authenticationpoc.main.repository.LogoutUsingFirebase

object DataHelper {

private val loginRepository: LoginRepository = LoginUsingFirebase()
private val signUpRepository: SignUpRepository = SignUpUsingFirebase()
private val homeRepository: HomeRepository = LogoutUsingFirebase()

fun getLoginRepository(): LoginRepository {
return loginRepository
}

fun getSignUpRepository(): SignUpRepository {
return signUpRepository
}

fun getHomeRepository(): HomeRepository {
return homeRepository
}
}

Let us now create our login functionality. We have already created our XML layout for our login activity. Let’s have a look at our LoginActivity.

package com.example.authenticationpoc.auth.login.view

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.example.authenticationpoc.R
import com.example.authenticationpoc.auth.login.model.AuthResponseData
import com.example.authenticationpoc.auth.login.viewmodel.LoginViewModel
import com.example.authenticationpoc.auth.signup.view.SignUpActivity
import com.example.authenticationpoc.core.Constants
import com.example.authenticationpoc.main.view.HomeActivity
import com.example.authenticationpoc.utility.Utility
import kotlinx.android.synthetic.main.activity_login.*

class LoginActivity : AppCompatActivity() {

private lateinit var loginViewModel: LoginViewModel
private var currentUser: AuthResponseData? = null
private lateinit var
context: Context
private lateinit var toolbar: Toolbar

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
loginViewModel = ViewModelProvider(this).get(LoginViewModel::class.java)
context = this
initViews()
setUpToolbar()
}

private fun setUpToolbar() {
toolbar = findViewById(R.id.toolbar_login)
setSupportActionBar(toolbar)
supportActionBar!!.setTitle(getString(R.string.authentication_poc_app))
}


private fun initViews() {
btn_login.setOnClickListener {
loginWithEmail()
}

txt_signup.setOnClickListener {
openSignUpScreen()
}
}

private fun loginWithEmail() {
val email = txt_email.text
val password = txt_password.text

loginViewModel.error.observe(this, Observer { errorMessage ->
if
(errorMessage != null) {
Utility
.showSnackBar(root_layout, errorMessage)
login_prog_bar.visibility = View.INVISIBLE
loginViewModel.error.postValue(null)
}
})

if (email.isNullOrEmpty() || password.isNullOrEmpty())
Utility
.showSnackBar(root_layout, getString(R.string.empty_email_or_password))
else {
loginViewModel.user.observe(this, Observer { user ->
currentUser = user
if (currentUser != null) {
Utility.storeStringSharedPref(Constants.USER_ID, email.toString(), context)
login_prog_bar.visibility = View.INVISIBLE
openHomeScreen()
}
})
loginViewModel.login(
email = email.toString(),
password = password.toString()
)
login_prog_bar.visibility = View.VISIBLE
}
}

private fun openHomeScreen() {
val intent = Intent(this, HomeActivity::class.java)
startActivity(intent)
finish()
}

private fun openSignUpScreen() {
val intent = Intent(this, SignUpActivity::class.java)
startActivity(intent)
}
}

Here you can set up your toolbar (optional).

Our main focus should be on our LoginWithEmail() function.

Here we are observing two Live data error and user. If our observer finds some response in the error, an appropriate error message will be displayed on the snackbar. If there is no error, it will not have any value. When there is a response in user, it means that our login has been successful and the username of the person who logged in is received in user live data.

This is our data class for the authentication response.

package com.example.authenticationpoc.auth.login.model

data class AuthResponseData(val userName: String)

Now let’s look at our view model. This class implements our ReponseListener of data type AuthResponseData and it overrides the onResponse() and onFailure() methods of our ResponseListener where it posts the value of our response from our firebase to our mutable live data whose value is being observed by our activity. Here we can see how we are using our DataHelper class to get the instance of our repository.

package com.example.authenticationpoc.auth.login.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.authenticationpoc.auth.login.model.AuthResponseData
import com.example.authenticationpoc.core.DataHelper
import com.example.authenticationpoc.core.interfaces.ResponseListener

class LoginViewModel : ViewModel(),
ResponseListener<AuthResponseData> {

var user: MutableLiveData<AuthResponseData?> = MutableLiveData()
var error = MutableLiveData<String>()

fun login(email: String, password: String): LiveData<AuthResponseData?> {
DataHelper.getLoginRepository().loginUser(email, password, this)
return user
}

override fun onResponse(response: AuthResponseData) {
user.postValue(response)
}

override fun onFailure(message: String) {
user.postValue(null)
error.value = message.substring(message.indexOf(':') + 1)
}
}

Our repository is the class where our application interacts with the outside data source or an internal database and fetches the data from them for our application.

Here to make our code further flexible we are not directly calling the repository. We are calling our actual repository via an interface. If we want to have another data source in the future we can add another class for it. This makes our code maintainable. Here LoginUsingFirebase is a child class of LoginRepository. Here if the login response from firebase is passed through our response listener to the view model where it is posted to the Live data which is constantly being observed in our Login Activity.

LoginRepository Interface :

package com.example.authenticationpoc.auth.login.repository

import com.example.authenticationpoc.auth.login.model.AuthResponseData
import com.example.authenticationpoc.core.interfaces.ResponseListener

interface LoginRepository {

fun loginUser(
email: String,
password: String,
listener: ResponseListener<AuthResponseData>
)
}

Its child class LoginUsingFirebase() :

package com.example.authenticationpoc.auth.login.repository

import android.app.Activity
import com.example.authenticationpoc.auth.login.model.AuthResponseData
import com.example.authenticationpoc.core.interfaces.ResponseListener
import com.google.firebase.auth.FirebaseAuth

class LoginUsingFirebase : LoginRepository {

override fun loginUser(
email: String,
password: String,
listener: ResponseListener<AuthResponseData>
) {
val firebaseAuth: FirebaseAuth = FirebaseAuth.getInstance()
firebaseAuth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(Activity()) { task ->
if
(task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
val userName = firebaseAuth.currentUser?.displayName ?: ""
val
responseData = AuthResponseData(userName)
listener.onResponse(responseData)
} else {
// If sign in fails, display a message to the user.
listener.onFailure(task.exception.toString())
}
}
}
}

In the same way, now let's create the SignUp

SignUpActivity

package com.example.authenticationpoc.auth.signup.view

import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.example.authenticationpoc.R
import com.example.authenticationpoc.auth.signup.model.SignUpResponseData
import com.example.authenticationpoc.auth.signup.viewmodel.SignUpViewModel
import com.example.authenticationpoc.databinding.ActivitySignUpBinding
import com.example.authenticationpoc.main.view.HomeActivity
import com.example.authenticationpoc.utility.Utility
import kotlinx.android.synthetic.main.activity_sign_up.*

class SignUpActivity : AppCompatActivity() {

private lateinit var signUpViewModel: SignUpViewModel
private var binding: ActivitySignUpBinding? = null
private var currentUser
: SignUpResponseData? = null
private lateinit var toolbar
: Toolbar

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_sign_up)
signUpViewModel = ViewModelProvider(this).get(SignUpViewModel::class.java)
binding?.viewmodel = signUpViewModel
initViews()
setUpToolbar()
}

private fun initViews() {
binding!!.btnSignup.setOnClickListener {
signUp()
}
}

private fun setUpToolbar() {
toolbar = findViewById(R.id.toolbar_signup)
setSupportActionBar(toolbar)
supportActionBar!!.setTitle(R.string.sign_up)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
}

override fun onSupportNavigateUp(): Boolean {
finish()
return true
}

private fun signUp() {
val name: String = txt_name.text.toString()
val email: String = txt_email.text.toString()
val password: String = txt_password.text.toString()
val confirmPassword: String = txt_confirm_password.text.toString()
val contactNum: String = txt_contact.text.toString()

signUpViewModel.error.observe(this, Observer { errorMessage ->
if
(errorMessage != null) {
Utility
.showSnackBar(root_signup_layout, errorMessage)

signup_prog_bar.visibility = View.INVISIBLE
signUpViewModel.error.postValue(null)

}
})
if (name.isEmpty() || email.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() || contactNum.isEmpty())
Utility
.showSnackBar(root_signup_layout, "Enter all the fields!!")
else
if
(password != confirmPassword)
Utility.showSnackBar(
root_signup_layout,
getString(R.string.password_confirmpassword_should_match)
)
else {
signUpViewModel.user.observe(this, Observer { firebaseUser ->
currentUser = firebaseUser
if (currentUser != null) {
signup_prog_bar.visibility = View.INVISIBLE
openHomeScreen()
}
})
signUpViewModel.signUpWithEmail(name, email, password, contactNum)
signup_prog_bar.visibility = View.VISIBLE
}

}

private fun openHomeScreen() {
val intent = Intent(this, HomeActivity::class.java)
startActivity(intent)
finish()
}
}

SignUp View Model :

package com.example.authenticationpoc.auth.signup.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.authenticationpoc.auth.signup.model.SignUpResponseData
import com.example.authenticationpoc.core.DataHelper
import com.example.authenticationpoc.core.interfaces.ResponseListener

class SignUpViewModel : ViewModel(), ResponseListener<SignUpResponseData> {

var user: MutableLiveData<SignUpResponseData?> = MutableLiveData()
var error = MutableLiveData<String>()

fun signUpWithEmail(
name: String,
email: String,
password: String,
contactNum: String
): LiveData<SignUpResponseData?> {
DataHelper.getSignUpRepository().signUpUser(name, email, password, contactNum, this)
return user
}

override fun onResponse(response: SignUpResponseData) {
user.postValue(response)
}

override fun onFailure(message: String) {
user.postValue(null)
error.value = message.substring(message.indexOf(':') + 1)
}
}

Data Class :

package com.example.authenticationpoc.auth.signup.model

data class SignUpResponseData(val email: String, val uid: String)

Repository :

SignUpRepository Interface :

package com.example.authenticationpoc.auth.signup.repository

import com.example.authenticationpoc.auth.signup.model.SignUpResponseData
import com.example.authenticationpoc.core.interfaces.ResponseListener

interface SignUpRepository {
fun signUpUser(
name: String,
email: String,
password: String,
contactNum: String,
listener: ResponseListener<SignUpResponseData>
)
}

SignUpUsingFirebase() :

package com.example.authenticationpoc.auth.signup.repository

import android.app.Activity
import android.content.ContentValues
import android.util.Log
import com.example.authenticationpoc.R
import com.example.authenticationpoc.auth.signup.model.SignUpResponseData
import com.example.authenticationpoc.core.Constants
import com.example.authenticationpoc.core.interfaces.ResponseListener
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase

class SignUpUsingFirebase : SignUpRepository {

private val db = Firebase.firestore

override fun signUpUser(
name: String,
email: String,
password: String,
contactNum: String,
listener: ResponseListener<SignUpResponseData>
) {
val firebaseAuth: FirebaseAuth = FirebaseAuth.getInstance()

firebaseAuth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(Activity()) { task ->
if
(task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
val userEmail = firebaseAuth.currentUser?.email ?: ""
val
uid = firebaseAuth.currentUser?.uid ?: ""
val
responseData = SignUpResponseData(userEmail, uid)
listener.onResponse(responseData)
addUserToFirebase(responseData, name, contactNum)
} else {
// If sign in fails, display a message to the user.
listener.onFailure(task.exception.toString())
}
}
}

private fun addUserToFirebase(
user: SignUpResponseData,
name: String,
contactNum: String
) {
val usersHashMap = hashMapOf(
Constants.CONST_NAME to name,
Constants.CONST_CONTACT_NUM to contactNum,
Constants.CONST_UID to user.uid,
Constants.CONST_EMAIL to user.email
)

db.collection(Constants.CONST_USERS).document(user.email)
.set(usersHashMap)
.addOnSuccessListener {
Log.i(
ContentValues.TAG, Activity().getString(R.string.data_added)
)
}
.addOnFailureListener { e -> Log.i(ContentValues.TAG, "Error while adding data", e) }
}
}

So we have just wrapped up our Sign up part. Now let’s create our home screen.
Home Screen XML layout :

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".home.view.HomeActivity"
>

<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:theme="@style/ToolbarColoredWhiteArrow"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/home"
app:titleMarginStart="@dimen/dp_20"
app:titleTextColor="@color/secondaryText"
/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/congratulation_you_successfully_logged_in"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar_main"
/>



</androidx.constraintlayout.widget.ConstraintLayout>

Let’s see the code of HomeActivity :

package com.example.authenticationpoc.home.view

import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.example.authenticationpoc.R
import com.example.authenticationpoc.auth.login.view.LoginActivity
import com.example.authenticationpoc.core.Constants
import com.example.authenticationpoc.home.viewmodel.HomeViewModel
import com.example.authenticationpoc.utility.Utility

class HomeActivity : AppCompatActivity() {

private lateinit var toolbar: Toolbar
private lateinit var homeViewModel: HomeViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
homeViewModel = ViewModelProvider(this).get(HomeViewModel::class.java)
setUpToolbar()
}

private fun setUpToolbar() {
toolbar = findViewById(R.id.toolbar_main)
setSupportActionBar(toolbar)
supportActionBar!!.setTitle(getString(R.string.home))
}

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_home, menu)
return true
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_logout -> {
logout()
}
}
return true
}

private fun logout() {
homeViewModel.logoutResponse.observe(this, Observer { response ->
if
(response.equals(Constants.LOGGED_OUT)) {
Utility.clearSharedPref(this)
openLogin()
}
})
homeViewModel.logout(this)
}

private fun openLogin() {
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
}
}

Home ViewModel :

package com.example.authenticationpoc.home.viewmodel

import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.authenticationpoc.core.Constants
import com.example.authenticationpoc.core.DataHelper
import com.example.authenticationpoc.core.interfaces.ResponseListener

class HomeViewModel : ViewModel(), ResponseListener<String> {

val logoutResponse: MutableLiveData<String> = MutableLiveData()
fun logout(context: Context): MutableLiveData<String> {
DataHelper.getHomeRepository().logoutFromFirebase(context, this)
return logoutResponse
}

override fun onResponse(response: String) {
logoutResponse.postValue(response)
}

override fun onFailure(message: String) {
logoutResponse.postValue(Constants.FAILED)
}
}

Repository :

HomeRepository Interface :

package com.example.authenticationpoc.home.repository

import android.content.Context
import com.example.authenticationpoc.core.interfaces.ResponseListener

interface HomeRepository {
fun logoutFromFirebase(context: Context, listener: ResponseListener<String>)
}

LogoutUsingFirebase() :

package com.example.authenticationpoc.home.repository

import android.content.Context
import com.example.authenticationpoc.core.Constants
import com.example.authenticationpoc.core.interfaces.ResponseListener
import com.firebase.ui.auth.AuthUI

class LogoutUsingFirebase : HomeRepository {

override fun logoutFromFirebase(context: Context, listener: ResponseListener<String>) {
AuthUI.getInstance()
.signOut(context)
.addOnCompleteListener {
listener.onResponse(Constants.LOGGED_OUT)
}
}
}

We also have a splash screen that takes the user directly to the home screen on restarting the application if the user has already logged in. Let’s have a look at it.

package com.example.authenticationpoc.splash

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.authenticationpoc.R
import com.example.authenticationpoc.auth.login.view.LoginActivity
import com.example.authenticationpoc.core.Constants
import com.example.authenticationpoc.home.view.HomeActivity
import com.example.authenticationpoc.utility.Utility

class SplashActivity : AppCompatActivity() {

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

val isUserLoggedIn = isUserLoggedIn()
if (isUserLoggedIn) {
launchHomeScreen()
} else {
launchLoginScreen()
}
}

private fun isUserLoggedIn(): Boolean {
val userId = Utility.getStringSharedPref(Constants.USER_ID, this)
return userId != null && userId.isNotEmpty()
}

private fun launchHomeScreen() {
val intent = Intent(this, HomeActivity::class.java)
startActivity(intent)
finish()
}

private fun launchLoginScreen() {
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
}
}

For the sake of completion of the project, we also have a utility and a constants class.

Utility Class :

package com.example.authenticationpoc.utility

import android.content.Context
import androidx.constraintlayout.widget.ConstraintLayout
import com.example.authenticationpoc.R
import com.example.authenticationpoc.core.Constants
import com.google.android.material.snackbar.Snackbar


object Utility {

fun showSnackBar(context: ConstraintLayout, message: String) {
Snackbar.make(context, message, Snackbar.LENGTH_LONG)
.setAction(Constants.CONST_ACTION, null)
.show()
}

fun storeStringSharedPref(key: String?, value: String, context: Context) {
val sharedPreferences = context.applicationContext.getSharedPreferences(
context.applicationContext.getString(
R.string.preference_file_key
), Context.MODE_PRIVATE
)
val editor = sharedPreferences.edit()
editor.putString(key, value)
editor.apply()
}

fun getStringSharedPref(key: String?, context: Context): String? {
val sharedPreferences = context.applicationContext.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
return sharedPreferences.getString(key, null)
}

fun clearSharedPref(context: Context) {
val sharedPreferences = context.applicationContext.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
val editor = sharedPreferences.edit()
editor.clear()
editor.apply()
}
}

Constants class :

package com.example.authenticationpoc.core

object Constants {

const val CONST_NAME = "name"
const val CONST_CONTACT_NUM
= "contactNumber"
const val CONST_UID
= "uid"
const val CONST_EMAIL
= "email"
const val CONST_USERS
= "Users"
const val CONST_ACTION
= "Action"
const val USER_ID
= "userId"
const val LOGGED_OUT
= "loggedOut"
const val FAILED
= "failed"
}

For other resources, we can go through the project on Github.

(In case you clone this project, follow the README.md to set up the project as I have removed my google-services.json file for security reasons.)

You can connect to me on LinkedIn https://www.linkedin.com/in/anantramanindia/

--

--