Understanding ViewModel with XML Layouts in Android

In modern Android development, data management and UI updates often present significant challenges. Traditionally, XML layouts were bound directly to Activities or Fragments, leading to tight coupling and lifecycle-related issues. With the introduction of Android Jetpack’s ViewModel, developers can separate data handling logic from UI controllers, enhancing code maintainability and testability.

What is ViewModel?

The ViewModel is a class that is responsible for preparing and managing the data for an Activity or a Fragment. It survives configuration changes such as screen rotations, so UI data isn’t lost when the Activity or Fragment is recreated. The ViewModel acts as a data provider, encapsulating business logic and exposing data for the UI.

Why Use ViewModel with XML Layouts?

  • Lifecycle Awareness: ViewModel survives configuration changes, preserving UI state.
  • Separation of Concerns: Decouples UI logic from data handling, improving code maintainability.
  • Testability: Enables easier unit testing of data preparation and management.
  • Data Persistence: Makes it easier to manage data across multiple lifecycles.

How to Use ViewModel with XML Layouts

Using ViewModel with XML layouts involves several steps. Here’s a comprehensive guide.

Step 1: Add ViewModel Dependency

First, ensure that you have the necessary dependency in your build.gradle file:

dependencies {
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"  // Optional, for LiveData support
    implementation "androidx.activity:activity-ktx:1.8.0" //For viewModels() delegate
}

Step 2: Create a ViewModel Class

Define your ViewModel class to hold and manage the UI-related data:


import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MyViewModel : ViewModel() {

    private val _userName = MutableLiveData("Initial Value")  // Backing property for MutableLiveData
    val userName: LiveData = _userName            // Expose as immutable LiveData

    fun updateUserName(newName: String) {
        _userName.value = newName
    }
}

In this example, MyViewModel holds a userName as LiveData, which can be observed for changes.

Step 3: Enable Data Binding in build.gradle

To use ViewModel with XML layouts, enable data binding in your app’s build.gradle file:

android {
    buildFeatures {
        dataBinding true
    }
}

Step 4: Modify XML Layout to Use Data Binding

Wrap your XML layout with a <layout> tag and define a <data> block to declare the ViewModel variable:

<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.myapp.MyViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.userName}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Update Name"
            android:onClick="@{() -> viewModel.updateUserName(`new Name`)}"  <!-- This is a placeholder. See Step 5 -->
            app:layout_constraintTop_toBottomOf="@+id/textView"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Note:

  • The layout file is wrapped in <layout> tags.
  • Inside the <data> tag, a variable named viewModel is declared, associating it with the MyViewModel class.
  • The TextView‘s text attribute is bound to viewModel.userName using the @{} syntax.
  • The `Button` has a placeholder for `new Name`, will address this next.

Step 5: Set the ViewModel in the Activity or Fragment

In your Activity or Fragment, obtain an instance of the ViewModel and set it as a binding variable:


import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import androidx.activity.viewModels
import com.example.myapp.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val viewModel: MyViewModel by viewModels()  //Use the delegate for ViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this // important, so LiveData updates happen
        binding.viewModel = viewModel  //setting the viewModel from the XML.
       
        // Manually finding and setting the onclick listener to be able to retrieve user entered text for name change.
        binding.button.setOnClickListener {
             val newName = "new Name";  //Can manually get new Name if wanted, hardcoding for simplicities sake.

              viewModel.updateUserName(newName)
         }
    }
}

Key points:

  • Initialize binding using DataBindingUtil.setContentView().
  • Set the lifecycleOwner of the binding to the current Activity/Fragment to enable LiveData observations.
  • Set the viewModel variable in the binding to your MyViewModel instance.
  • Demonstrate manual handling for the button because cannot access EditText views in XML directly from here.

Handling User Input

To handle user input and update the ViewModel, you might need to incorporate data from UI elements like EditText. Since directly accessing EditText views from the XML layout using data binding for callbacks is complex, you can observe the ViewModel and manually update the view:

1. Update ViewModel to Handle Input:


import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MyViewModel : ViewModel() {

    private val _userName = MutableLiveData("")  // Start with empty name

    val userName: LiveData = _userName

    fun updateUserName(newName: String) {
        _userName.value = newName
    }
}

2. Modified XML Layout

Update the XML to include an EditText field and button that interact with the ViewModel


<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.myapp.MyViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <EditText
            android:id="@+id/editTextName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:hint="Enter name"
            android:text="@={viewModel.userName}" <!-- Two-way binding for EditText -->
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.userName}"
            app:layout_constraintTop_toBottomOf="@+id/editTextName"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Update Name"
            app:layout_constraintTop_toBottomOf="@+id/textView"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

3. Activity/Fragment setup to get reference from View, set a Listener and Update UserName upon Button Click:


import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import androidx.activity.viewModels
import com.example.myapp.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val viewModel: MyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.viewModel = viewModel

        binding.button.setOnClickListener {
            val newName = binding.editTextName.text.toString()
            viewModel.updateUserName(newName)
        }
    }
}

In summary, handling EditText with ViewModel requires using data binding for simple observation, manual extraction in click listener from Activity (Best practice), and utilizing a delegate such as `by viewModels()` .

Advanced Scenarios and Best Practices

  • LiveData Transformations: Use Transformations.map and Transformations.switchMap for complex data transformations within ViewModel.
  • Repository Pattern: Incorporate a repository layer for data abstraction and to manage data sources (local database, remote API).
  • Dependency Injection: Employ dependency injection frameworks like Hilt or Koin to provide ViewModel instances.
  • Testing: Write unit tests for ViewModels to ensure business logic correctness. Mock dependencies such as Repositories, to not test implementation details.

Conclusion

Using ViewModel with XML layouts enhances Android application architecture by separating concerns and providing lifecycle awareness. By adopting data binding, UI components can be efficiently bound to the ViewModel, simplifying UI updates and improving overall code maintainability. Properly leveraging ViewModel helps create robust, testable, and maintainable Android applications.