Hello Android developers! Today, we are going to walk through building a bare-bones Android application using the MVVM architectural pattern, Kotlin, and data binding. We'll go through each line of code to understand the underlying principles behind using MVVM so that we have a better grasp of the concepts behind it as we move forward toward more complicated projects.
The code used in this blog post can be found here
Step 1: Set Up Your Android Studio Project
First, we need to set up our Android Studio project with the necessary dependencies. In your build.gradle
(Module: app) file, add the ViewModel, LiveData, and Kotlin Coroutines libraries. These libraries will help us implement the MVVM pattern and handle asynchronous tasks efficiently.
dependencies {
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
implementation "androidx.fragment:fragment-ktx:1.6.1"
// Kotlin Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
}
Step 2: Enable Data Binding
Next, enable data binding in your build.gradle
(Module: app) file. Data binding allows us to bind UI components in our layouts to data sources in our app using a declarative format rather than programmatically.
android {
...
buildFeatures {
dataBinding true
}
}
Step 3: Create the Model
Now, let's create a data class to represent our data model. In Kotlin, a data class automatically generates utility functions such as toString()
, hashCode()
, and equals()
based on the properties defined in the class. Here, we create a User
data class with id
and name
properties.
data class User(
val id: Int,
val name: String
)
Step 4: Create the ViewModel
Next, create a ViewModel
class where you will manage your app's data:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class MainViewModel : ViewModel() {
private val _userLiveData = MutableLiveData<User>()
val userLiveData: LiveData<User> get() = _userLiveData
suspend fun getUser(id: Int) {
// Simulate a network or database call to fetch the user
withContext(Dispatchers.IO) {
val user = User(id, "John Doe")
_userLiveData.postValue(user)
}
}
}
class MainViewModel : ViewModel()
: Here, we define ourMainViewModel
class that inherits from theViewModel
class. This means that our class can take advantage of the lifecycle-aware nature of ViewModels, preserving data across configuration changes such as screen rotations.private val _userLiveData = MutableLiveData<User>()
: We define a private mutable live data variable to hold our user data. Being mutable means that it can be changed within the ViewModel.val userLiveData: LiveData<User> get() = _userLiveData
: We expose the mutable live data through a public immutableLiveData
property. This ensures that the data can be observed, but not modified, from outside the ViewModel.suspend fun getUser(id: Int)
: This is a suspending function, which means it can perform long-running operations without blocking the main thread. It is part of Kotlin's coroutines, which allow us to handle asynchronous tasks more efficiently and with less boilerplate code.withContext(
Dispatchers.IO
)
: Inside thegetUser
function, we usewithContext(
Dispatchers.IO
)
to switch the coroutine context toDispatchers.IO
, which is optimized for I/O tasks such as network requests and database operations. This ensures that the long-running operation of fetching the user data is performed off the main thread, preventing UI freezes and ANRs (Application Not Responding errors).val user = User(id, "John Doe")
: Here, we simulate fetching a user from a database or network source by creating a newUser
object with the given ID and a hardcoded name._userLiveData.postValue(user)
: After fetching the user data, we update our mutable live data with the new data using thepostValue
method. This method is thread-safe and can be called from any thread, which is why we use it here instead of thesetValue
method, which must be called from the main thread.
Step 5: Set Up Data Binding in Your Layout
Update your layout file to use data binding. Create a layout file named activity_main.xml
with the following content:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<!-- Declare ViewModel variable -->
<variable
name="viewModel"
type="com.example.app.MainViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/userNameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.userLiveData.name}" />
</RelativeLayout>
</layout>
<layout>
: This is the root tag for layout files that use data binding. It indicates that this layout file will use data binding, which enables us to bind UI components in our layouts to data sources in our app using a declarative format rather than programmatically.<data>
: Inside the<layout>
tag, we have a<data>
element where we declare variables that will be used in the layout file. These variables represent the data that the layout will bind to.<variable name="viewModel" type="
com.example.app
.MainViewModel" />
: Here, we declare a variable namedviewModel
of typeMainViewModel
. This variable will be used to bind data from theMainViewModel
to the UI components in the layout.<TextView android:text="@{
viewModel.userLiveData.name
}" />
: In thisTextView
, we use data binding expression language to bind thetext
attribute to thename
property of theuserLiveData
in theMainViewModel
. This is an example of one-way data binding, where data flows from the ViewModel to the UI, but not the other way around.
Step 6: Create the View
Finally, create an activity to represent the view. In this activity, you'll observe the data in the ViewModel
and update the UI accordingly:
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.example.app.databinding.ActivityMainBinding
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.viewModel = viewModel
viewModel.userLiveData.observe(this, { user ->
// Update UI with the user data
binding.userNameTextView.text = user.name
})
lifecycleScope.launch {
viewModel.getUser(1)
}
}
}
private lateinit var binding: ActivityMainBinding
: Here, we declare alateinit
variable for our binding class. Thelateinit
keyword allows us to initialize the variable at a later point, and the binding class is generated automatically by the Android build system when we enable data binding.private val viewModel: MainViewModel by viewModels()
: We initialize theMainViewModel
using theviewModels
delegate, which is a Kotlin property delegate that lazily provides a ViewModel instance. This delegate is lifecycle-aware, meaning the ViewModel will be retained as long as the scope of the ViewModel (in this case, the activity) is alive.binding = ActivityMainBinding.inflate(layoutInflater)
: In theonCreate
method, we inflate our layout using theinflate
method of the binding class, which takes aLayoutInflater
as a parameter. This sets up the data binding for our layout.setContentView(binding.root)
: We set the content view to the root view of our binding class. Theroot
property holds the root view in our layout file (the highest-level view in the layout).binding.viewModel = viewModel
: Here, we set theviewModel
variable in our layout file to ourMainViewModel
instance. This binds the ViewModel to the layout, allowing us to use ViewModel properties directly in the XML layout.viewModel.userLiveData.observe(this, { user -> binding.userNameTextView.text =
user.name
})
: We observe theuserLiveData
property in our ViewModel. Theobserve
method takes aLifecycleOwner
(in this case,this
, which refers to our activity) and an observer lambda, which is called whenever theuserLiveData
changes. Inside the observer lambda, we update the text of aTextView
to display the user's name.lifecycleScope.launch { viewModel.getUser(1) }
: Finally, we uselifecycleScope
to launch a coroutine and fetch the user data when the activity is created. ThelifecycleScope
is aCoroutineScope
tied to the lifecycle of the activity, meaning any coroutines launched in this scope will be automatically canceled when the activity is destroyed.
And that's it, you've successfully implemented a basic MVVM example. You can now build the app and it should show the text that we set in our viewmodel.
Conclusion
Congratulations! You have just built a bare-bones Android application using the MVVM architectural pattern, Kotlin, and data binding. This setup maintains a clean separation of concerns between the View and ViewModel layers, in line with the MVVM architectural pattern.
Remember, this is a very basic example. In a real-world application, you would add error handling, a repository layer, and potentially a database or network client to fetch data from a remote server or local database.
In our future post, we'll create a more detailed MVVM example that will also have dependency injection implemented. Till then, happy coding!