Building a Bare-Bones App with MVVM in Android: (Day 02)

Photo by Andrew Neel on Unsplash

Building a Bare-Bones App with MVVM in Android: (Day 02)

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 our MainViewModel class that inherits from the ViewModel 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 immutable LiveData 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 the getUser function, we use withContext(Dispatchers.IO) to switch the coroutine context to Dispatchers.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 new User 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 the postValue method. This method is thread-safe and can be called from any thread, which is why we use it here instead of the setValue 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 named viewModel of type MainViewModel. This variable will be used to bind data from the MainViewModel to the UI components in the layout.

  • <TextView android:text="@{viewModel.userLiveData.name}" />: In this TextView, we use data binding expression language to bind the text attribute to the name property of the userLiveData in the MainViewModel. 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 a lateinit variable for our binding class. The lateinit 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 the MainViewModel using the viewModels 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 the onCreate method, we inflate our layout using the inflate method of the binding class, which takes a LayoutInflater 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. The root property holds the root view in our layout file (the highest-level view in the layout).

  • binding.viewModel = viewModel: Here, we set the viewModel variable in our layout file to our MainViewModel 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 the userLiveData property in our ViewModel. The observe method takes a LifecycleOwner (in this case, this, which refers to our activity) and an observer lambda, which is called whenever the userLiveData changes. Inside the observer lambda, we update the text of a TextView to display the user's name.

  • lifecycleScope.launch { viewModel.getUser(1) }: Finally, we use lifecycleScope to launch a coroutine and fetch the user data when the activity is created. The lifecycleScope is a CoroutineScope 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!