MVVM-KOTLIN-Android
View,ViewModel,Model is a one of architectural pattern recently introduced by Google Company which will empower you to write manageable, maintainable, cleaner and testable code. There are too many libraries people use for lifecycle-aware components, LiveData, ViewModel so it depend your application business witch one you choose
Lets Go
We create a new project with Android Studio and use Empty Activity as starting point. Also select kotlin language and chose the name of Launcher activity CommentActivity.
When our project created let’s get into importing a bunch of libraries. Open build.gradle for the app module. And add the following packages. Keep in mind that the versions will change in the future, so keep them up to date.
dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0-rc01'
implementation "androidx.lifecycle:lifecycle-extensions:2.0.0-rc01"
implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
}
Activity_Main
<?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=".ui.comment.commentActivity">
<TextView
android:id="@+id/textView_comments"
android:layout_width="405dp"
android:layout_height="304dp"
app:layout_constraintHeight_percent="0.55"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:scrollbars="vertical"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
tools:text="i like MVVM Architecture Pattern - Khashayar "
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"/>
<EditText
android:id="@+id/editText_comment"
android:layout_width="0dp"
app:layout_constraintWidth_percent="0.7"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:hint="Comment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView_comments"
app:layout_constraintVertical_bias="0.0" />
<EditText
android:id="@+id/editText_author"
android:layout_width="0dp"
app:layout_constraintWidth_percent="0.7"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:hint="Author"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/editText_comment"
app:layout_constraintVertical_bias="0.0" />
<Button
android:id="@+id/button_add_comment"
android:layout_width="106dp"
android:layout_height="100dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:backgroundTint="#2196F3"
android:text="Add Comment"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="@+id/editText_author"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/editText_comment"
app:layout_constraintTop_toTopOf="@+id/editText_comment"
app:layout_constraintVertical_bias="1.0"
app:layout_constraintWidth_percent="0.25"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Create the Comment data class
The idea of this application is creating comment and save in fake database so we need class with name Comment
class Comment (val commentText:String ,
val author:String) {
override fun toString(): String {
return "author is : $author *** --- $commentText "
}
}
DAO for for Comment (Data Access Object)
When you have a Comment object in memory, you’d usually want to store it in a database to make it persist when the app is closed. You could use any kind of a database from Cloud Firestore to local SQLite or you could even set up your own backend, communicate with it through an API and use SQLite as a local cache. If you want to use SQLite in your real apps, check out a library called ROOM or Realm ORM. It will make your life easier.
In the world of ROOM, anytime you want to do something in a database, you do it through a DAO. Under normal circumstances, a DAO is an interface defining all allowed actions which can happen for a table in the database, like reads and writes.
this tutorial is focused on the core concepts of MVVM architecture. Adding a real database of any kind would be only unnecessarily creating complexity. This is why you will use a fake database and a fake DAO which will save data to a MutableList. You are not going to skip any important steps which are present in production apps and the code will be simple at the same time.
class FakeCommentDao {
// A fake database table
private val commentList = mutableListOf<Comment>()
// MutableLiveData is from the Architecture Components Library
// LiveData can be observed for changes
private val comments = MutableLiveData<List<Comment>>()
init {
// Immediately connect the now empty commentList
// to the MutableLiveData which can be observed
comments.value = commentList
}
fun addComment(comment: Comment) {
commentList.add(comment)
// After adding a comment to the "database",
// update the value of MutableLiveData
// which will notify its active observers
comments.value = commentList
}
// Casting MutableLiveData to LiveData because its value
// shouldn't be changed from other classes
fun getComments() = comments as LiveData<List<Comment>>
}
Database class as a container for DAOs
Because it doesn’t make sense to have 2 instances of database at the same time, a database class will be a singleton. Kotlin has a nice syntax for singletons where instead of class you write object. While this would be sufficient in our case, it usually isn’t for production apps. When you use the object keyword, you don’t have a chance to pass something into the class’ constructor. In the case of ROOM, you need to pass an application context to your database. To circumvent this problem, you have to create singletons the Java way even in Kotlin.
// Private primary constructor inaccessible from other classes
class FakeDatabase private constructor() {
// All the DAOs go here!
var commentDao = FakeCommentDao()
private set
companion object {
// @Volatile - Writes to this property are immediately visible to other threads
@Volatile private var instance: FakeDatabase? = null
// The only way to get hold of the FakeDatabase object
fun getInstance() =
// Already instantiated? - return the instance
// Otherwise instantiate in a thread-safe manner
instance ?: synchronized(this) {
// If it's still not instantiated, finally create an object
// also set the "instance" property to be the currently created one
instance ?: FakeDatabase().also { instance = it }
}
}
}
Repository
Repository is one of the design pattern, defined by Eric Evens. It is one of the most useful and most widely applicable design patterns ever invented. Domain layer request needed data to repository, and repository tosses data from local repositories like database or SharedPreferences. It makes loose coupling between ViewModel, so easier writing unit test code to ViewModel and business logic.
class CommentRepository private constructor(private val commentDao: FakeCommentDao){
// This may seem redundant.
// Imagine a code which also updates and checks the backend.
fun addComment(comment: Comment) {
commentDao.addComment(comment)
}
fun getComment() = commentDao.getComments()
companion object {
// Singleton instantiation you already know and love
@Volatile private var instance: CommentRepository? = null
fun getInstance(commentDao: FakeCommentDao) =
instance ?: synchronized(this) {
instance ?: CommentRepository(commentDao).also { instance = it }
}
}
}
it doesn’t make sense to have multiple repository objects, so it will be a singleton. This time you need to pass in the FakeCommentDao for repository to fulfill its role. You will use dependency injection to supply this FakeQuoteDao instance to the repository.
user interface
Now we should connect what you created to the “view” part of MVVM, in this case the CommentActivity. Activities and Fragments are not only for displaying on the screen and for handling user input. All of the logic, data, manipulation with the data handle with ViewModel. Then the View only calls functions on the ViewModel. This way, the data doesn’t get reset when an orientation change occurs.
/ CommentRepository dependency will again be passed in the
// constructor using dependency injection
class CommentViewModel(private val commentRepository: CommentRepository) : ViewModel() {
fun getComment() = commentRepository.getComment()
fun addComment(comment: Comment) = commentRepository.addComment(comment)
}
CommentViewModel requires a repository to function and that repository is passed into the constructor. The way that ViewModels are created / gotten from ViewModelProvider (to prevent recreation on, say, orientation changes) requires a ViewModelFactory class. You simply cannot create ViewModels directly, instead, they are going to be created in a factory.
class CommentViewModelFactory (private val commentRepository: CommentRepository)
: ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return CommentViewModel(commentRepository) as T
}
}
Working on Dependency injection
You would need to change every single constructor call in every class you want to test. Instead, you can create all of the dependencies in one place. So if you need to test something, you know where and what to change – only one class which constructs all of the dependencies. This is a simple DI. also you can use a framework like Dagger2 for really complex projects
object Injector {
// This will be called from QuotesActivity
fun provideCommentViewModelFactory(): CommentViewModelFactory {
// ViewModelFactory needs a repository, which in turn needs a DAO from a database
// The whole dependency tree is constructed right here, in one place
val commentRepository = CommentRepository.getInstance(FakeDatabase.getInstance().commentDao)
return CommentViewModelFactory(commentRepository)
}
}
FINISHING …..
Right now you creating everything needed in except the view with which the user can interact
class CommentActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initializeUi()
}
private fun initializeUi() {
// Get the CommentsViewModelFactory with all of it's dependencies constructed
val factory = Injector.provideCommentViewModelFactory()
// Use ViewModelProviders class to create / get already created CommentsViewModel
// for this view (activity)
val viewModel = ViewModelProviders.of(this, factory)
.get(CommentViewModel::class.java)
// Observing LiveData from the CommentsViewModel which in turn observes
// LiveData from the repository, which observes LiveData from the DAO ☺
viewModel.getComment().observe(this, Observer { comment ->
val stringBuilder = StringBuilder()
comment.forEach { comment ->
stringBuilder.append("$comment\n\n")
}
textView_comments.text = stringBuilder.toString()
})
// When button is clicked, instantiate a Comment and add it to DB through the ViewModel
button_add_comment.setOnClickListener {
val comment = Comment(editText_comment.text.toString(), editText_author.text.toString())
viewModel.addComment(comment)
editText_comment.setText("")
editText_author.setText("")
}
}
}