MVVM Design Pattern in Android
Overview
MVVM is a Model-View-ViewModel design pattern architecture that removes the tight coupling between every application components. In the below image, you can clearly see that every children components don't have a reference to the parent components. They have a reference to an observable though which they can communicate with the parent components.
Components of MVVM
Model
It is the data and business logic of an Android application. It's a data class where you can define the properties and methods too.
View
It is the user interface of an Android application, activities and fragments. These views send the user requests to ViewModel and to get the response, the application components has to subscribe to the observables which the ViewModel has exposed to it.
ViewModel
It is a bridge between the View and the Model in MVVM design pattern. ViewModel isn't aware which view is interacting with it. ViewModel only interacts with the Model and exposes the observable that can be observed by the view.
Steps to follow to integrate with a Room Database
- Create a model, a data class (table)
@Entity(tableName = "quote")
data class Quote(
@PrimaryKey(autoGenerate = true)
val id: Int,
val text: String,
val author: String
)
- Create a Dao
@Dao
interface QuoteDao {
@Query("SELECT * from quote")
fun getQuotes(): LiveData<List<Quote>>
@Insert
suspend fun insertQuote(quote: Quote)
}
- Create a Database
@Database(entities = [Quote::class], version = 1)
abstract class QuoteDatabase : RoomDatabase() {
abstract fun quoteDao(): QuoteDao
companion object {
private var INSTANCE: QuoteDatabase? = null
fun getDatabase(context: Context): QuoteDatabase {
if (INSTANCE == null) {
synchronized(this) {
INSTANCE = Room.databaseBuilder(
context,
QuoteDatabase::class.java,
"quote_database"
)
.createFromAsset("quotes.db")
.build()
}
}
return INSTANCE!!
}
}
}
- Create a Repository
class QuoteRepository(private val quoteDao: QuoteDao) {
fun getQuotes(): LiveData<List<Quote>>{
return quoteDao.getQuotes()
}
suspend fun insertQuote(quote: Quote){
quoteDao.insertQuote(quote)
}
}
- Create a ViewModel
class MainViewModel(private val quoteRepository: QuoteRepository) : ViewModel() {
fun getQuotes() : LiveData<List<Quote>>{
return quoteRepository.getQuotes()
}
fun insertQuote(quote: Quote){
viewModelScope.launch(Dispatchers.IO){
quoteRepository.insertQuote(quote)
}
}
}
- Create a factory for ViewModel
class MainViewModelFactory(private val quoteRepository: QuoteRepository) : ViewModelProvider.Factory{
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MainViewModel(quoteRepository) as T
}
}
- Bind the data in xml file. To automatically attach DataBinding to the xml
<?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="quotes"
type="String" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btnAddQuote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="32dp"
android:text="InsertQuote" />
<TextView
android:textSize="32sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="32dp"
android:text="@{quotes}"/>
</LinearLayout>
</ScrollView>
</layout>
- Create the references of ViewModel in MainActivity
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val dao = QuoteDatabase.getDatabase(applicationContext).quoteDao()
val repository = QuoteRepository(dao)
mainViewModel =
ViewModelProvider(this, MainViewModelFactory(repository)).get(MainViewModel::class.java)
mainViewModel.getQuotes().observe(this, Observer {
binding.quotes = it.toString()
})
binding.btnAddQuote.setOnClickListener {
val quote = Quote(0, "This is testing", "Testing")
mainViewModel.insertQuote(quote)
}
}
}
MVVM with Retrofit and Room Database
- Dependency
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// Retrofit
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.1"
// ViewModel and LiveData
def lifecycle_version = "2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Coroutines
def coroutines_version = "1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// Room Database
def room_version = "2.4.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
kapt "android.arch.persistence.room:compiler:1.1.1"
implementation "androidx.room:room-ktx:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
}
- Model
data class QuoteList(
val count : Int ,
val lastItemIndex : Int ,
val page : Int ,
val results : List<Result> ,
val totalCount : Int ,
val totalPages : Int
)
@Entity(tableName = "quote")
data class Result(
@PrimaryKey val quoteID : Int ,
val author : String ,
val authorSlug : String ,
val content : String ,
val dateAdded : String ,
val dateModified : String ,
val _id : String ,
val length : Int
)
- Interface of Retrofit
interface QuoteService
{
// BASE_URL + "/quotes?page=1
@GET("/quotes")
suspend fun getQuotes(@Query("page") page : Int) : Response<QuoteList>
}
- Retrofit Helper
object RetrofitHelper
{
const val BASE_URL = "https://quotable.io/"
fun getInstance() : Retrofit
{
return Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create()).build()
}
}
- Dao for Room Database
@Dao
interface QuoteDao
{
@Insert
suspend fun addQuotes(quotes : List<Result>)
@Query("SELECT * FROM quote")
suspend fun getQuotes() : List<Result>
}
- Database class for Room Database
@Database(entities = [Result::class] , version = 1)
abstract class QuoteDatabase : RoomDatabase()
{
abstract fun quoteDao() : QuoteDao
companion object
{
@Volatile
private var INSTANCE : QuoteDatabase? = null
fun getDatabase(context : Context) : QuoteDatabase
{
if (INSTANCE == null)
{
synchronized(this) {
/* Make sure to pass application context */
INSTANCE = Room.databaseBuilder(context , QuoteDatabase::class.java , "quote_database")
.build()
}
}
return INSTANCE !!
}
}
}
- Repository for Retrofit and Room Database
class QuoteRepository(
private val quoteService : QuoteService , private val quoteDatabase : QuoteDatabase
)
{
private val quotesLiveData = MutableLiveData<QuoteList>()
val quotes : LiveData<QuoteList>
get() = quotesLiveData
suspend fun getQuotes(page : Int)
{
val result = quoteService.getQuotes(page)
if (result.body() != null)
{
quoteDatabase.quoteDao().addQuotes(result.body() !!.results)
quotesLiveData.postValue(result.body())
}
}
}
- ViewModel for MainActivity
class MainViewModel(private val quoteRepository : QuoteRepository) : ViewModel()
{
init
{
viewModelScope.launch(Dispatchers.IO) {
quoteRepository.getQuotes(1)
}
}
val quotes : LiveData<QuoteList>
get() = quoteRepository.quotes
}
- ViewModelFactory for MainActivity
class MainViewModelFactory(private val quoteRepository : QuoteRepository) :
ViewModelProvider.Factory
{
override fun <T : ViewModel> create(modelClass : Class<T>) : T
{
return MainViewModel(quoteRepository) as T
}
}
- Application class for MainActivity
class QuoteApplication : Application()
{
lateinit var quoteRepository : QuoteRepository
override fun onCreate()
{
super.onCreate()
initialize()
}
private fun initialize()
{
val quoteService : QuoteService =
RetrofitHelper.getInstance().create(QuoteService::class.java)
val quoteDatabase = QuoteDatabase.getDatabase(this)
quoteRepository = QuoteRepository(quoteService , quoteDatabase)
}
}
- Implementation in MainActivity
class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "MainActivity"
}
private lateinit var binding : ActivityMainBinding
private lateinit var mainViewModel : MainViewModel
override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this , R.layout.activity_main)
val repository = (application as QuoteApplication).quoteRepository
mainViewModel = ViewModelProvider(
this , MainViewModelFactory(repository)
)[MainViewModel::class.java]
mainViewModel.quotes.observe(this , Observer {
Log.d(TAG , it.results.toString())
})
}
}
- Update AndroidManifest.xml file to use Application class
<application
android:name=".QuoteApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MVVM_Retrofit_Android">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// Retrofit
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.1"
// ViewModel and LiveData
def lifecycle_version = "2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Coroutines
def coroutines_version = "1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// Room Database
def room_version = "2.4.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
kapt "android.arch.persistence.room:compiler:1.1.1"
implementation "androidx.room:room-ktx:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
}
data class QuoteList(
val count : Int ,
val lastItemIndex : Int ,
val page : Int ,
val results : List<Result> ,
val totalCount : Int ,
val totalPages : Int
)
@Entity(tableName = "quote")
data class Result(
@PrimaryKey val quoteID : Int ,
val author : String ,
val authorSlug : String ,
val content : String ,
val dateAdded : String ,
val dateModified : String ,
val _id : String ,
val length : Int
)
interface QuoteService
{
// BASE_URL + "/quotes?page=1
@GET("/quotes")
suspend fun getQuotes(@Query("page") page : Int) : Response<QuoteList>
}
object RetrofitHelper
{
const val BASE_URL = "https://quotable.io/"
fun getInstance() : Retrofit
{
return Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create()).build()
}
}
@Dao
interface QuoteDao
{
@Insert
suspend fun addQuotes(quotes : List<Result>)
@Query("SELECT * FROM quote")
suspend fun getQuotes() : List<Result>
}
@Database(entities = [Result::class] , version = 1)
abstract class QuoteDatabase : RoomDatabase()
{
abstract fun quoteDao() : QuoteDao
companion object
{
@Volatile
private var INSTANCE : QuoteDatabase? = null
fun getDatabase(context : Context) : QuoteDatabase
{
if (INSTANCE == null)
{
synchronized(this) {
/* Make sure to pass application context */
INSTANCE = Room.databaseBuilder(context , QuoteDatabase::class.java , "quote_database")
.build()
}
}
return INSTANCE !!
}
}
}
class QuoteRepository(
private val quoteService : QuoteService , private val quoteDatabase : QuoteDatabase
)
{
private val quotesLiveData = MutableLiveData<QuoteList>()
val quotes : LiveData<QuoteList>
get() = quotesLiveData
suspend fun getQuotes(page : Int)
{
val result = quoteService.getQuotes(page)
if (result.body() != null)
{
quoteDatabase.quoteDao().addQuotes(result.body() !!.results)
quotesLiveData.postValue(result.body())
}
}
}
class MainViewModel(private val quoteRepository : QuoteRepository) : ViewModel()
{
init
{
viewModelScope.launch(Dispatchers.IO) {
quoteRepository.getQuotes(1)
}
}
val quotes : LiveData<QuoteList>
get() = quoteRepository.quotes
}
class MainViewModelFactory(private val quoteRepository : QuoteRepository) :
ViewModelProvider.Factory
{
override fun <T : ViewModel> create(modelClass : Class<T>) : T
{
return MainViewModel(quoteRepository) as T
}
}
class QuoteApplication : Application()
{
lateinit var quoteRepository : QuoteRepository
override fun onCreate()
{
super.onCreate()
initialize()
}
private fun initialize()
{
val quoteService : QuoteService =
RetrofitHelper.getInstance().create(QuoteService::class.java)
val quoteDatabase = QuoteDatabase.getDatabase(this)
quoteRepository = QuoteRepository(quoteService , quoteDatabase)
}
}
class MainActivity : AppCompatActivity()
{
companion object
{
const val TAG = "MainActivity"
}
private lateinit var binding : ActivityMainBinding
private lateinit var mainViewModel : MainViewModel
override fun onCreate(savedInstanceState : Bundle?)
{
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this , R.layout.activity_main)
val repository = (application as QuoteApplication).quoteRepository
mainViewModel = ViewModelProvider(
this , MainViewModelFactory(repository)
)[MainViewModel::class.java]
mainViewModel.quotes.observe(this , Observer {
Log.d(TAG , it.results.toString())
})
}
}
<application
android:name=".QuoteApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MVVM_Retrofit_Android">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
Comments
Post a Comment