The sample code for this post can be found here.
In my previous post, I demonstrated how the use of a model-view-presenter pattern could help separate logic from Android boilerplate and make code easier to test. The addition of the presenter did not address the modelling of a view’s state, however. That is what the view model is for. With the help of a view model, a view’s state can be easily stored and managed by a separate entity. The state is manipulated by the presenter and displayed by the view. The following diagram shows the relationships between the view, the presenter and the view model.
The relationship between the different components is always a 1 to 1 to 1 relationship. That means for a view there is exactly one presenter and exactly one view model. Typically the fragment or activity (our view) will instantiate the view model and pass it along to the presenter:
@Override public void onCreate(Bundle args) { super.onCreate(args); //TODO retrieve arguments from bundle mViewModel = new SearchViewModel(); mPresenter = new SearchPresenter(mViewModel, new SearchServiceRepository(), this); }
In the above snippet, our SearchFragment instantiates a new View Model in its onCreate() method and passes it along to the SearchPresenter. This ensures that both the fragment and the presenter share the same instance of the view model. The presenter can thus modify the view model and the view can then query the model’s state and display it accordingly. Let’s take a closer look at our SearchFragment and its corresponding presenter to see how we can enhance our app and incorporate a view model into our model-view-presenter pattern.
The View Model
As mentioned above, the SearchFragment and SearchPresenter share the same view model. A view model’s structure should always try to mirror or correspond to the structure of what the view is displaying. The view model used by the SearchPresenter and the SearchView has to model the state of our search view. Since our search view mainly consists of a list of search results, the view model will also contain a list of items:
private List<SearchItemModel> mItems;
A SearchItemModel represents a single search result in our search view. It can look like this:
class SearchItemModel { private SongDto mSong; private ItemType mType; private boolean mAdded; public SongDto getSong() { return mSong; } public void setSong(SongDto song) { this.mSong = song; } public ItemType getType() { return mType; } public void setType(ItemType type) { this.mType = type; } public boolean isAdded() { return mAdded; } public void setAdded(boolean added) { this.mAdded = added; } public enum ItemType { HEADER, RESULT } }
It wraps a SongDto and contains some additional information that can help the view display it. The ItemType can be used to display different types of rows in the search view’s list. We might want to display a header row that scrolls along with our results. So our first SearchItemModel might be of type HEADER and will simply be used to display a nice header. We might also want to mark the songs in our search view that we’ve already added to our list of bookmarked songs. That’s what we could use the isAdded flag for. Items that have the flag set to true, have been bookmarked and could display a little check mark next to the name of the song. Any property that you need in order to model the look of data in a view should be added to our view model. A view model does not only need to be restricted to simple data modelling, however. It can also take care of how its state is changed. Let’s add a new feature to our search view in order to demonstrate this.
Bookmarking songs using our View Model
Bookmarking a song will allow a user to mark songs they’ve searched for. When a song is bookmarked, it is added to a list maintained by the user. The logic of bookmarking songs will reside in the SearchPresenter. The presenter will provide the following method to accomplish this:
public void onAddSongToMyList(int position) { }
Let’s implement this method using the test-first approach. Our first test case could look something like this:
@Test public void testOnAddSongToMyList_then_addSelectedSong() { SongDto selectedSong = new SongDto(); SearchItemModel selectedSearchItem = new SearchItemModel(); ArrayList<SearchItemModel> searchItems = new ArrayList<>(); searchItems.add(new SearchItemModel()); searchItems.add(selectedSearchItem); selectedSearchItem.setSong(selectedSong); ViewModel mockViewModel = mock(ViewModel.class); when(mockViewModel.getItems()).thenReturn(searchItems); SearchRepository mockRepository = mock(SearchRepository.class); SearchPresenter testee = new SearchPresenter(mockViewModel, mockRepository, mock(SearchView.class)); testee.onAddSongToMyList(1); verify(mockRepository).addSongToMyList(eq(selectedSong), any(SearchRepository.SearchListener.class)); }
This test makes sure that when we add a song to our list, that the proper song is retrieved from the view model and passed on to our repository. The implementation could look something like this:
public void onAddSongToMyList(int position) { SearchItemModel selectedSearchItem = mViewModel.getItems().get(position); mSearchRepository.addSongToMyList(selectedSearchItem.getSong(), this); }
This is pretty nice and short. However our test is a bit complicated. This code can definitely make a bit more use of our view model to simplify things. It would be better if the view model dealt with all of the indices and just provided a method to select a song. A comfortable method in the View Model could be this:
SongDto selectSong(int position);
We could then split up our above test into the following 2 tests
First a test to ensure that we select the song in our view model:
@Test public void testOnAddSongToMyList_then_selectSong(){ int selectedPosition = 2; ViewModel mockViewModel = mock(ViewModel.class); SearchRepository mockRepository = mock(SearchRepository.class); SearchPresenter testee = new SearchPresenter(mockViewModel, mockRepository, mock(SearchView.class)); testee.onAddSongToMyList(selectedPosition); verify(mockViewModel).selectSong(selectedPosition); }
And second a test to make sure we pass the selected song to the repository:
@Test public void testOnAddSongToMyList_then_addSelectedSong(){ int selectedPosition = 2; SongDto selectedSong = new SongDto(); ViewModel mockViewModel = mock(ViewModel.class); SearchRepository mockRepository = mock(SearchRepository.class); when(mockViewModel.selectSong(selectedPosition)).thenReturn(selectedSong); SearchPresenter testee = new SearchPresenter(mockViewModel, mockRepository, mock(SearchView.class)); testee.onAddSongToMyList(selectedPosition); verify(mockRepository).addSongToMyList(eq(selectedSong), any(SearchRepository.SearchListener.class)); }
The implementation in our presenter would be this:
public void onAddSongToMyList(int position) { SongDto selectedSong = mViewModel.selectSong(position); mSearchRepository.addSongToMyList(selectedSong, this); }
This approach is much cleaner. The presenter doesn’t have to know about how the internals of the view model are set up and our unit tests don’t have to know about it either. The view model could use the position passed in to the method and add or subtract any number from it in order to find the selected song. The presenter and its unit tests don’t have to know about any of that. All the presenter needs to know is that a selection took place and what the selected song is so that it can be passed on to the repository. The presenter decides when which method needs to called but does not need to know the details of that method. The presenter decides when to select a song in the view model but it does not care about what that entails.
In addition to abstracting the selection of a song, the view model can also perform additional tasks that can make displaying the state of our view a bit easier. One such example is that the view model could mark the selected search result item as having been added to our list so that it can be graphically highlighted in the recycler view’s adapter. Let’s look at the view model’s implementation of selectSong().
@Override public SongDto selectSong(int position) { SongDto selectedSong = null; int index = 0; for (SearchItemModel searchItem : mItems) { if (index == position) { if (searchItem.getSong() != null) { selectedSong = searchItem.getSong(); searchItem.setAdded(true); } } index++; } return selectedSong; }
The view model finds the search result item with the song at the given index. It only considers search result items which have songs so any headers and separators that may be part of the list of search result items are ignored. In addition to finding and returning the selected song, the view model also marks the corresponding search result item as having been added (setAdded(true)). This flag can now be queried in our recycler view’s adapter. If the flag is set, we can display a hint to the user that the song has been added to their list. Again, all of this happens without the presenter having to even be aware of this flag. The presenter simply tells the view model to select a song and return it to the presenter. The details as to how the view’s state is affected by this selection are hidden away in the view model.
Displaying data based on the view model’s state
The View model can now be used in our view to display the current state. Since our view mainly consists of a list or a recycler view, we do this in the adapter. Our view uses a RecyclerView so we will implement a RecyclerView.Adapter. In the adapter’s onBindViewHolder() method, we can access our view model and query its state. The code in our adapter could look something like this:
@Override public void onBindViewHolder(ViewHolder holder, int position) { SearchItemModel item = mItems.get(position); if(SearchItemModel.ItemType.RESULT == item.getType()) { SongDto song = item.getSong(); holder.mSongNameText.setText(song.getName()); holder.mArtistNameText.setText(song.getArtist()); holder.mAlbumNameText.setText(song.getAlbum()); holder.mCheckmark.setVisibility(View.GONE); if(item.isAdded()){ holder.mCheckmark.setVisibility(View.VISIBLE); } } }
Our view model contains a list of SearchItemModels. These items are what are displayed by the RecyclerView in our view. In the RecyclerView’s adapter, we access the view model’s items to display each invididual row. This happens in the adapter’s onBindViewHolder shown above. This method is called for each row that needs to be shown. For each row we get the corresponding SearchItemModel and check whether its isAdded() flag has been set. If it has been set, we display a check mark. If not, we hide the check mark. Because the view model’s structure corresponds to the way the view displays its data, its state can easily be queried and displayed. Furthermore, the adapter doesn’t need to know about the logic behind view model’s state. It doesn’t need to know why the flag is set to true, just that it has been set to true. It just needs to query its state and display something accordingly.
Restoring the state
The view model represents the state of the view. That means that once our fragment returns from the background (because we switched to a detail view and returned to our search fragment), we can restore our view model and display the view just the way it was before we switched to the other fragment. Our fragment has a reference to its view model. It passes this view model to its presenter. The presenter can then simply display the view model’s state. The presenter’s present() method could look like this:
public void present() { if(mViewModel != null){ if(mViewModel.getItems().size() > 0) { mSearchView.showResults(); } else { mSearchView.showInfoMessage(); } } else { mSearchView.showInfoMessage(); } }
It checks whether the view model has any items to display. If so, it simply displays them instead of displaying the info message.
Conclusion
View models add a nice way to model a view’s state. They also allow for further abstraction of certain model-altering methods. When selecting a song to be bookmarked, the presenter does not have to know about which exact song in the list of songs needs to be bookmarked. It simply wants a song to be bookmarked so that it can be passed along to the repository. Displaying complex lists with different item types also becomes much easier with the help of a view model. An adapter can iterate through the items and it can query each item’s state and modify the view accordingly. In order to maximize the use of the view model in displaying data, its structure has to correspond to the way the view displays its data. Finally, view models can easily be persisted and restored allowing UI state to persist across fragment transactions. In the next post we will continue to add features to the Songster app that will require more complex business logic. With the help of a UseCase, we can abstract that logic and make sure that our presenter and view model stays cohesive.
Pingback: Noser Blog Data binding and Model-View-ViewModel in Android - Noser Blog