Building the ViewModel
Now that we have our Database and Repository, we can build the ViewModel. The ViewModel is the bridge between your UI and your data. It manages the data for your app's screens and keeps everything in sync, even if the device rotates or the app is paused.
Why Use ViewModels with Room?
ViewModels make it easy to manage your app's data and UI. They keep your data safe during screen changes and help your UI always show the latest information from your database.
- Keep your data and UI in sync
- Separate your data logic from your UI code
- Make your app easier to maintain and update
Building the ViewModel
The ViewModel manages the data for your app's screens. It talks to the Repository to get and update data, and makes sure your UI always shows the latest information.
Example: NoteViewModel (from RoomDatabaseDemo)
// NoteViewModel acts as a communication bridge between the UI (NoteScreen) and the data repository.
// It exposes data to the UI and handles UI-related data operations, abstracting the data source.
class NoteViewModel(private val repository: NoteRepository) : ViewModel() {
// Expose a StateFlow of notes from the repository. This allows the UI to observe changes.
// stateIn converts a Flow into a StateFlow, ensuring it's always active while subscribed.
val notes: StateFlow<List<Note>> = repository.allNotes.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000), // Start collecting when there's an active subscriber and stop 5s after the last subscriber disappears.
emptyList()
)
// Function to add a new note. It launches a coroutine in the viewModelScope
// to perform the insert operation asynchronously via the repository.
fun addNote(title: String, content: String, date: String) {
viewModelScope.launch {
repository.insert(Note(title = title, content = content, date = date))
}
}
// Function to delete an existing note. It launches a coroutine in the viewModelScope
// to perform the delete operation asynchronously via the repository.
fun deleteNote(note: Note) {
viewModelScope.launch {
repository.delete(note)
}
}
// Companion object to provide a factory for the NoteViewModel.
// This is necessary because NoteViewModel has a constructor that takes NoteRepository.
companion object {
fun provideFactory(application: Application): ViewModelProvider.Factory {
// Create a NoteRepository, which in turn depends on NoteDao from NoteDatabase.
return NoteViewModelFactory(NoteRepository(NoteDatabase.getDatabase(application).noteDao
()))
}
}
}
// NoteViewModelFactory is a custom ViewModelProvider.Factory that allows us to instantiate
// NoteViewModel with a NoteRepository dependency.
class NoteViewModelFactory(private val repository: NoteRepository) : ViewModelProvider.Factory {
// The create method is responsible for creating new ViewModel instances.
override fun <T : ViewModel> create(modelClass: Class<T>): T {
// Check if the requested ViewModel class is NoteViewModel.
if (modelClass.isAssignableFrom(NoteViewModel::class.java)) {
// If it is, create and return a new NoteViewModel instance.
@Suppress("UNCHECKED_CAST") // Suppress the unchecked cast warning as we've checked the type.
return NoteViewModel(repository) as T
}
// If an unknown ViewModel class is requested, throw an exception.
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Understanding the `NoteViewModel` Class
The NoteViewModel is a central component in your Android app's architecture, especially when working with Room and Compose. Its primary role is to hold and manage UI-related data in a lifecycle-conscious way, surviving configuration changes like screen rotations. It acts as a communication bridge between your UI (NoteScreen) and your data layer (NoteRepository).
1. ViewModel Class Definition and Dependency
// NoteViewModel acts as a communication bridge between the UI (NoteScreen) and the data repository.
// It exposes data to the UI and handles UI-related data operations, abstracting the data source.
class NoteViewModel(private val repository: NoteRepository) : ViewModel() {
// ...
}
class NoteViewModel(private val repository: NoteRepository) : ViewModel():- This defines our
NoteViewModelclass. It takes aNoteRepositoryas a constructor parameter. This is a crucial concept called Dependency Injection. Instead of the ViewModel creating its own Repository, it receives an already existing one. This makes the ViewModel easier to test and more flexible, as you can swap out different Repository implementations if needed. - It extends
ViewModel()from the Android Architecture Components. This base class provides the lifecycle awareness that allows the ViewModel to retain its data across configuration changes.
- This defines our
2. Exposing Notes as a StateFlow
val notes: StateFlow<List<Note>> = repository.allNotes.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000), // Start collecting when there's an active subscriber and stop 5s after the last subscriber disappears.
emptyList()
)
val notes: StateFlow<List<Note>>: This property is how the UI observes the list of notes. It's aStateFlow, which is a hot observable that always has a value and efficiently emits updates.repository.allNotes: The ViewModel gets theFlowof all notes directly from theNoteRepository..stateIn(...): This is a powerful Kotlin Flow operator that converts a cold `Flow` into a hot `StateFlow`. It needs a few parameters:viewModelScope: This is a CoroutineScope tied to the ViewModel's lifecycle. Any coroutines launched within this scope will be automatically cancelled when the ViewModel is cleared, preventing memory leaks.SharingStarted.WhileSubscribed(5000): This strategy dictates when the `StateFlow` should start and stop collecting from the upstream `Flow` (repository.allNotes). `WhileSubscribed(5000)` means it will start collecting when there's at least one active subscriber and will keep collecting for 5 seconds after the last subscriber disappears before stopping. This optimizes resource usage.emptyList(): This is the initial value that the `StateFlow` will hold before any data is collected from the repository.
3. Adding and Deleting Notes
fun addNote(title: String, content: String, date: String) {
viewModelScope.launch {
repository.insert(Note(title = title, content = content, date = date))
}
}
fun deleteNote(note: Note) {
viewModelScope.launch {
repository.delete(note)
}
}
fun addNote(...)andfun deleteNote(...): These functions are public methods that the UI (e.g., your `NoteScreen`) calls to perform data modifications.viewModelScope.launch { ... }: Inside these functions, we launch a coroutine using `viewModelScope.launch`. This is crucial because database operations are often long-running and should not be executed on the main UI thread. Launching them in a coroutine on a background thread ensures your app remains responsive. The ViewModel then simply delegates these operations to the `NoteRepository`.
4. ViewModel Factory for Custom Dependencies
companion object {
fun provideFactory(application: Application): ViewModelProvider.Factory {
return NoteViewModelFactory(NoteRepository(NoteDatabase.getDatabase(application).noteDao
()))
}
}
}
class NoteViewModelFactory(private val repository: NoteRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(NoteViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return NoteViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
- Why a Factory? Normally, `ViewModel`s are created by the system. However, if your `ViewModel` needs custom dependencies (like our `NoteRepository`), you need to provide a custom `ViewModelProvider.Factory`. This factory tells the system how to create an instance of your `ViewModel` with its specific dependencies.
companion object { ... }: The `provideFactory` method is defined in a `companion object` so it can be called directly on the `NoteViewModel` class without needing an instance.fun provideFactory(application: Application): ViewModelProvider.Factory: This function returns an instance of our `NoteViewModelFactory`. It takes an `Application` context, which is then used to get the `NoteDatabase` instance and subsequently the `NoteDao` to construct the `NoteRepository`.class NoteViewModelFactory(private val repository: NoteRepository) : ViewModelProvider.Factory { ... }: This is our custom factory class.- It takes a `NoteRepository` in its constructor, which it will then use to create the `NoteViewModel`.
override fun <T : ViewModel> create(modelClass: Class<T>): T: This is the core method of the factory. It's called by the system when it needs to create a ViewModel.- It checks if the requested `modelClass` is `NoteViewModel`.
- If it is, it creates a new `NoteViewModel` instance, passing the `repository` it holds.
- The `@Suppress("UNCHECKED_CAST")` is used because the Kotlin compiler can't fully guarantee the type safety here, but we've already checked `modelClass`, so it's safe.
- If an unexpected ViewModel class is requested, it throws an `IllegalArgumentException`.
Tips for Success
- Use ViewModels to keep your UI and data in sync.
- Let the Repository handle all data operations.
- Keep your ViewModel focused on managing UI state and business logic.
Common Mistakes to Avoid
- Mixing UI code and data operations in the same place.
- Not using coroutines for database operations.
- Forgetting to update your UI when data changes.
Best Practices
- Keep your ViewModel and Repository code clean and organized.
- Use StateFlow or LiveData for reactive state management.
- Test your ViewModel logic separately from your UI.