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 namedviewModelis declared, associating it with theMyViewModelclass. - The
TextView‘s text attribute is bound toviewModel.userNameusing 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
bindingusingDataBindingUtil.setContentView(). - Set the
lifecycleOwnerof the binding to the current Activity/Fragment to enable LiveData observations. - Set the
viewModelvariable in the binding to yourMyViewModelinstance. - 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.mapandTransformations.switchMapfor 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.