Leave a rating/review
Notes: 16. Move Operations to ViewModels
The student materials have been reviewed and are updated as of September 2022.
Demo
Now that you’ve added the ViewModel, it’s time to move all of the operations to it. Most of the VMs are prebuilt for you again, so you don’t have to write a lot of boilerplate code. But there’s one you’ll do for practice.
Open the BookReviewDetailsViewModel and add the following code:
fun addNewEntry(entry: String) {
val data = _bookReviewDetailsState.value?.review ?: return
val updatedReview = data.copy(
entries = data.entries + ReadingEntry(comment = entry),
lastUpdatedDate = Date()
)
updateReview(updatedReview)
onDialogDismiss()
}
Now add the following functions to update a review and reset the state for the dialog.
private fun updateReview(updatedReview: Review) {
viewModelScope.launch {
repository.updateReview(updatedReview)
setReview(repository.getReviewById(updatedReview.id))
}
}
fun onDialogDismiss() {
_deleteEntryState.value = null
_isShowingAddEntryState.value = false
}
And finally, add the remaining code that handles the rest of the operations and state updates.
fun removeReadingEntry(readingEntry: ReadingEntry) {
val data = _bookReviewDetailsState.value?.review ?: return
val updatedReview = data.copy(
entries = data.entries - readingEntry,
lastUpdatedDate = Date()
)
updateReview(updatedReview)
onDialogDismiss()
}
fun onItemLongTapped(readingEntry: ReadingEntry) {
_deleteEntryState.value = readingEntry
}
fun onAddEntryTapped() {
_isShowingAddEntryState.value = true
}
All of these functions are from the Activity, that you used before, just copied over. You also added a few functions that change the state of the dialogs that you show, such as the delete and add reading list dialogs.
Now open the BookReviewDetailsActivity and let’s change the code to propagate all of these operations to the ViewModel:
// onCreate
bookReviewDetailsViewModel.setReview(data)
// FAB
onClick = { bookReviewDetailsViewModel.onAddEntryTapped() })
// Reading Entries
ReadingEntries(bookReview.review.entries) {
bookReviewDetailsViewModel.onItemLongTapped(it) // long item click
}
You’re almost there, all that’s left is to update the dialogs!
bookReviewDetailsViewModel.addNewEntry(it)
bookReviewDetailsViewModel.onDialogDismiss()
onDeleteItem =
{
bookReviewDetailsViewModel.removeReadingEntry(it)
},
onDismiss = { bookReviewDetailsViewModel.onDialogDismiss() }
I know none of these changes directly impact the Compose part of the UI, but it’s important to understand the process of converting from one way of building things to another.
What happened here is that you’ve transferred all of the logic to the ViewModel, and you’ve passed the data changes to it, so it can update the internal state.
Then the last step is to connect the state from the ViewModel, to the UI. You’ll do that in the next episode for most of the ViewModels, but let’s jump into that topic, and try to implement one screen now, as a preview.
Within the BookReviewDetailsActivity, remove all the states and the repository, and leave just the ViewModel.
[Remove state, repository]
That’s the first step you have to take to start migrating to a fully declarative Compose UI, and reactive MVVM pattern! :]
There are many errors in the code because there are missing references, but you’ll change that in a bit, to accommodate the new behavior.
Change the code as such, to fix some of the issues:
val animationState by bookReviewDetailsViewModel.screenAnimationState.observeAsState(Initial)
val state = animateBookReviewDetails(animationState)
LaunchedEffect(Unit, block = {
bookReviewDetailsViewModel.onFirstLoad()
})
There is a lot going on here. You’re using the observeAsState() function, to translate a LiveData object into a State object, for Compose.
This is important because having State that refers to LiveData, makes your UI reactive to LiveData emissions, meaning your UI will automatically update when you change the internal state.
Also notice how you used the by keyword, that lets the value know it’s dictated by a delegate - the State object. Having delegates means that every time you call get, by accessing the property, you get fresh state.
Also add the following function to update the screen state from within the ViewModel:
fun onFirstLoad() {
_screenAnimationState.value = Loaded
}
That’s it. Now the animation is going to work exactly like before, but it’s all decoupled! Let’s move on to other state, to fix the rest of the issues:
val reviewState by bookReviewDetailsViewModel.bookReviewDetailsState.observeAsState()
val bookName =
reviewState?.book?.name ?: stringResource(id = R.string.book_review_details_title)
Just like before, you’re observing the state from the ViewModel, within Compose.
That being said, the reviewState is actually an indirect accessor to the State, and as such it can return a nullable value, which is why you need to add the appropriate nullchecks. Previously you provided an empty initial value to the function, which is why it wasn’t nullable.
Now add the following code, to change the core part of the content display logic:
val bookReview by bookReviewDetailsViewModel.bookReviewDetailsState.observeAsState(
EMPTY_BOOK_REVIEW
)
val genre by bookReviewDetailsViewModel.genreState.observeAsState(EMPTY_GENRE)
val deleteEntryState by bookReviewDetailsViewModel.deleteEntryState.observeAsState()
val isShowingAddEntry by bookReviewDetailsViewModel.isShowingAddEntryState.observeAsState(false)
val entryToDelete = deleteEntryState
Now update the rest of the references, to match the new state.
You used the by keyword and the observeAsState() functions to prepare the state for the UI. Notice how you again passed in some values, mostly EMPTY values, to the function. This will represent the default value for the delegate, so you don’t get a nullable value.
You also fetched the current entryToDelete value, as delegates don’t follow nullcheck safety, and their values can change internally. You also removed duplicate code.
That’s it! That’s all you need to do to move to a fully reactive and declarative way of building and updating you UI.
Now build & run the app, and you should see everything on the details screen work as before!
[Build & Run, open review details]
The UI code now only handles the display logic while the Business logic layer, or the ViewModel, handles the user interaction and fetching data from the repository.
Everything is cleanly separate, and all in two small files. You’ll implement the rest of the state handling in the next episode, so see you there! :]