Leave a rating/review
Notes: 15. Use the MVVM Pattern
The student materials have been reviewed and are updated as of September 2022.
Intro
[Slide 1 - Declarative patterns]
There have been other declarative frameworks similar to Compose, such as Flutter, React Native, Swift UI and much more.
And all of these frameworks always present the same challenge - how do you connect your declarative UI to the rest of the architecture you used in your app.
Good thing about Jetpack Compose is that you can connect the UI directly to observable state, such as LiveData, and have the UI update itself automatically!
[Slide 2 - LiveData]
Within Compose, there are special functions that let you turn LiveData constructs into State, allowing you to connect the state to the UI and update it whenever your database changes, or a new piece of information comes from the API.
[Slide 3 - UI Coupling 1/3 - Declarative UI]
It’s also important to know how to couple or decouple your UI from the rest of the application and architecture.
So the philosophy in Compose, when it comes to the Model-View-ViewModel pattern, is that your UI is fully declarative. It doesn’t know much about the rest of the architecture, other than it has to communicate user actions to the ViewModel.
[Slide 4 - UI Coupling 2/3 - ViewModel communication]
Then when the ViewModel receives those actions, it knows that it needs to either fetch some data, or change the state and in doing so, it updates the state, reflecting those changes to the UI.
[Slide 5 - UI Coupling 3/3 - Data Updates]
And any time you update the data from the Database for example, the database will emit new state, reactively updating the UI.
This is great because you don’t have to think about writing extra logic to change each specific part of the UI or update just one View. Compose does it for you!
Let’s see how to implement the MVVM pattern, and slowly start migrating the logic away from the UI.
[Slide 6 - Three step process]
Now before you dive into code, you need to understand that the MVVM pattern implementation is a three-step process.
You first have to build all the ViewModels, and set up the state you want to handle within them.
Then you need to move all the operations to ViewModels and change how they update the state from within.
And finally, you need to connect the state to Compose UI, to show the data, and react to data changes.
You’ll do all this over the course of the next three episodes, with some pre-built code in each of the episodes.
Let’s get to it! :]
Demo
Most of the ViewModels are already predefined for you, but there’s one that you’ll build yourself. Create a new file called BookReviewDetailsViewModel, and add the following code:
@HiltViewModel
class BookReviewDetailsViewModel @Inject constructor(
private val repository: LibrarianRepository
) : ViewModel() {
}
Each ViewModel will communicate with the repository, which is why you added it to the constructor parameters.
You also used the @Inject and @HiltViewModel annotations to inject the dependency through Hilt, a new DI library for Android! That’s all less important now, as your focus is to set up the state that will update the UI as you use the app.
Add the following code, to represent the state:
private val _bookReviewDetailsState = MutableLiveData<BookReview>()
val bookReviewDetailsState: LiveData<BookReview> = _bookReviewDetailsState
private val _genreState = MutableLiveData<Genre>()
val genreState: LiveData<Genre> = _genreState
private val _deleteEntryState = MutableLiveData<ReadingEntry>()
val deleteEntryState: LiveData<ReadingEntry> = _deleteEntryState
private val _isShowingAddEntryState = MutableLiveData<Boolean>(false)
val isShowingAddEntryState: LiveData<Boolean> = _isShowingAddEntryState
private val _screenAnimationState = MutableLiveData<BookReviewDetailsScreenState>()
val screenAnimationState: LiveData<BookReviewDetailsScreenState> = _screenAnimationState
To represent the state within the ViewModel, you’re using a common pattern of a backing mutable state and an exposed immutable state.
The properties with an underscore prefix are mutable, and you’ll change them from within the ViewModel. And the non-underscore properties are used to expose the data to the UI, and are immutable.
Notice how you’re using LiveData again, instead of the State type from Compose. This is because there’s a super cool way to turn LiveData into State, using helper functions. But more on that later!
Now add the following code to add a way to set the BookReview:
fun setReview(bookReview: BookReview) {
_bookReviewDetailsState.value = bookReview
viewModelScope.launch {
_genreState.value = repository.getGenreById(bookReview.book.genreId)
}
}
Whenever you set the BookReview, you update the state, and that will in turn update the UI, once you connect the LiveData in the next few episodes. And finally, you also have to fetch the Genre by ID, to show that on the UI.
One last thing before you start moving the operations to the ViewModels. Open the BookReviewDetailsActivity, and add the following code to prepare the ViewModel dependency for the activity:
private val bookReviewDetailsViewModel by viewModels<BookReviewDetailsViewModel>()
This will let Hilt inject the ViewModel and let you use it for state.
Now build & Run the app, just to check that everything works correctly.
[Build & Run]
This is the first step out of the three you need to do. In the next episode, you’ll move all the data operations to the ViewModel, and then in the third episode, you’ll connect the state to the UI.
See you there! :]