Working with ViewModel
1. Creating ViewModel Classes
ViewModel classes in Android are designed to store and manage UI-related data in a lifecycle-conscious way. They are part of the Android Architecture Components and help separate business logic from UI components.
| Property | Description | Notes |
|---|---|---|
| ViewModel | Base class for ViewModel | Extend this class to create custom ViewModels |
| LiveData | Observable data holder | Used to observe changes in data |
| MutableLiveData | Mutable version of LiveData | Allows data to be updated from within the ViewModel |
Example: Basic ViewModel Class
import androidx.lifecycle.ViewModel
import androidx.lifecycle.MutableLiveData
class UserViewModel : ViewModel() {
private val _user = MutableLiveData<String>()
val user: LiveData<String>
get() = _user
fun setUser(name: String) {
_user.value = name
}
}
Note: ViewModel instances are automatically retained during configuration changes like screen rotations.
2. Sharing ViewModel Between Fragments
| Fragment | ViewModel Instance | Notes |
|---|---|---|
| Fragment A | sharedViewModel |
Accessed via by viewModels() with viewModelProviderFactory |
| Fragment B | sharedViewModel |
Same instance as Fragment A's ViewModel |
Example: Sharing ViewModel Between Fragments
class SharedViewModel : ViewModel() {
private val _data = MutableLiveData<String>()
val data: LiveData<String> = _data
fun updateData(newData: String) {
_data.value = newData
}
}
class FragmentA : Fragment() {
private val sharedViewModel: SharedViewModel by viewModels(
factoryProducer = { SharedViewModelFactory() }
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
sharedViewModel.data.observe(viewLifecycleOwner) { data ->
// Update UI with data
}
}
}
class FragmentB : Fragment() {
private val sharedViewModel: SharedViewModel by viewModels(
factoryProducer = { SharedViewModelFactory() }
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
sharedViewModel.updateData("Updated from Fragment B")
}
}
Note: Both fragments must use the same
ViewModelProvider.Factory to ensure they receive the same instance.
Warning: Avoid direct access to
ViewModel instances in fragments without proper scoping; use viewModels() delegate for lifecycle-aware access.
3. Using SavedStateHandle
The SavedStateHandle is a key component in Android's ViewModel that allows you to save and restore data across configuration changes (like screen rotations) and process death. It provides a way to persist data in a structured and type-safe manner.
| Property | Type | Description |
|---|---|---|
| get(key: String) | T? |
Retrieves a value of type T associated with the given key. |
| set(key: String, value: T) | Unit |
Sets a value of type T for the given key. |
| contains(key: String) | Boolean |
Returns true if the key exists in the handle. |
| remove(key: String) | T? |
Removes and returns the value associated with the key. |
Example: Using SavedStateHandle in ViewModel
class MyViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
private val nameKey = "name"
private val ageKey = "age"
fun setName(name: String) {
savedStateHandle.set(nameKey, name)
}
fun getName(): String? = savedStateHandle.get(nameKey)
fun setAge(age: Int) {
savedStateHandle.set(ageKey, age)
}
fun getAge(): Int? = savedStateHandle.get(ageKey)
}
Note:
SavedStateHandle is automatically managed by the framework and ensures that data persists across configuration changes. It is particularly useful for passing data between fragments or activities.
Warning: Avoid storing sensitive data in
SavedStateHandle as it may be persisted to disk and could pose a security risk.
Key Features of SavedStateHandle
- Automatic Persistence: Data is automatically persisted and restored across configuration changes.
- Type Safety: Provides type-safe access to stored data.
- Key Management: Ensures keys are managed to avoid collisions.
4. Injecting Dependencies into ViewModel
| Dependency Type | Injection Method | Description |
|---|---|---|
| Repository | ViewModelProvider |
Pass repository instance via constructor in ViewModel |
| Use Case | ViewModelProvider |
Inject use case object to handle business logic |
| Context | ViewModelProvider |
Inject application context for accessing resources |
Example: Injecting Repository into ViewModel
class MyViewModel(private val repository: MyRepository) : ViewModel() {
fun fetchData() {
viewModelScope.launch {
val data = repository.fetchData()
// Handle data
}
}
}
Example: Using ViewModelProvider with Factory
class MyViewModelFactory(private val repository: MyRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return MyViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
// In Activity/Fragment
val factory = MyViewModelFactory(repository)
val viewModel = ViewModelProvider(this, factory).get(MyViewModel::class.java)
5. Managing ViewModel Scope
| Property | Description | Usage |
|---|---|---|
| viewModelScope | Coroutine scope tied to the ViewModel's lifecycle | Used to launch coroutines that should be cancelled when the ViewModel is cleared |
| lifecycleScope | Coroutine scope tied to the Lifecycle owner's lifecycle | Used for launching coroutines that should be cancelled when the Lifecycle is destroyed |
Example: Using viewModelScope
class MyViewModel : ViewModel() {
fun fetchData() {
viewModelScope.launch {
val data = repository.fetchData()
// Update UI with data
}
}
}
Example: Using lifecycleScope
class MyFragment : Fragment() {
private val viewModel: MyViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launch {
val data = viewModel.fetchData()
// Update UI with data
}
}
}
Note:
viewModelScope is preferred for ViewModel-specific coroutines, while lifecycleScope is used in fragments or activities to manage coroutines tied to their lifecycle.
6. Handling Configuration Changes
| Configuration Change | Description | ViewModel Behavior |
|---|---|---|
| Screen Rotation | Changing device orientation from portrait to landscape | ViewModel is retained and not recreated |
| Keyboard Visibility | Showing or hiding the soft keyboard | ViewModel is retained and not recreated |
| Locale Change | Changing language or region settings | ViewModel is retained and not recreated |
Example: Observing Configuration Changes in ViewModel
class MyViewModel : ViewModel() {
private val _configChange = MutableLiveData<String>()
val configChange: LiveData<String> = _configChange
fun handleConfigChange(newLocale: String) {
_configChange.value = newLocale
}
}
Note: ViewModel automatically survives configuration changes, making it ideal for holding UI-related data.
Warning: Avoid storing context or activity references in ViewModel to prevent memory leaks.
7. Clearing ViewModel
Example: Clearing ViewModel Data
class MyViewModel : ViewModel() {
private val _data = MutableLiveData<String>()
val data: LiveData<String> = _data
fun clearData() {
_data.value = null
}
}
class MyActivity : AppCompatActivity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[MyViewModel::class.java]
viewModel.clearData()
}
}
| Method | Description | Usage |
|---|---|---|
| clearData() | Resets the data in the ViewModel to null | Call this method to clear the LiveData value |
Note: Clearing data in ViewModel does not destroy the ViewModel instance itself. It only resets the state of the data.
8. Using ViewModel with Compose
Example: Basic ViewModel usage in Compose
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
class UserViewModel : ViewModel() {
private val _user = mutableStateOf("John Doe")
val user: State<String> = _user
fun updateUser(name: String) {
_user.value = name
}
}
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = "Current user: ${viewModel.user.value}")
Button(onClick = { viewModel.updateUser("Jane Smith") }) {
Text(text = "Update User")
}
}
}
| Feature | Description | Notes |
|---|---|---|
| viewModel() | viewModel() is a Compose function to access ViewModel instances. |
Automatically handles lifecycle and scope |
| State<T> | Used to hold state in ViewModel for Compose. | Immutable and reactive |
| mutableStateOf() | Creates a mutable state holder for reactive updates. | Used inside ViewModel to manage UI state |
Note: ViewModel in Compose is designed to be lifecycle-aware and should not hold references to composables or views.
9. Testing ViewModel
Example: Unit Test with JUnit and Mockito
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
class UserViewModelTest {
@get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var viewModel: UserViewModel
@Before
fun setup() {
val repository = Mockito.mock(UserRepository::class.java)
viewModel = UserViewModel(repository)
}
@Test
fun `getUsers returns correct data`() {
val expectedUsers = listOf("Alice", "Bob")
Mockito.`when`(repository.getUsers()).thenReturn(expectedUsers)
val users = viewModel.users.value
assertEquals(expectedUsers, users)
}
}
| Test Type | Description | Tools |
|---|---|---|
| Unit Test | Tests ViewModel logic in isolation | Junit, Mockito |
| Integration Test | Tests ViewModel with Repository and other components | Robolectric, Espresso |
Note: Use
InstantTaskExecutorRule to ensure LiveData observers execute synchronously in tests.Warning: Avoid testing UI logic directly in ViewModel tests. Focus on business logic and state transitions.
10. Implementing ViewModel Factory
Example: Custom ViewModel Factory
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class MyViewModel(val data: String) : ViewModel() {
// ViewModel logic here
}
class MyViewModelFactory(private val data: String) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return MyViewModel(data) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
| Property | Description | Usage |
|---|---|---|
| ViewModelProvider.Factory | Interface for creating ViewModels | Implement to customize ViewModel creation |
| create(modelClass) | Method to instantiate ViewModel | Must return instance of requested ViewModel class |
Note: The factory pattern allows for dependency injection and custom initialization logic in ViewModels.