Background
I've observed an increase in architecture-focused Android posts lately in channels such as /r/androiddev and Android Weekly. That's great, but frankly it's about time. When I transitioned from Windows Phone development to Android a couple of years ago I felt it was difficult to find good examples on how to architect a solid app. It didn't help that Google's examples violated most best practices[1] either. I kind of went with my own version of MVVM (or MVPVM) which I had with me from the .NET platform and it sort of worked. ReadingHannes Dorfmann's post on MVP and his Mosby framework, I realize I'm not the only one who've been struggling with getting the established patterns working with Android (he spent 3 years). The framework is not exactly leading you into a good architecture. Maybe Fragments is partly to blame, and why Square Inc is advocating against it.
In any case the community is steadily going in a direction where the good UI-architecture is common knowledge. Frameworks[2] are helping in that matter and blog posts like Dorfmann's and Artem Zin's post on the Model. Still, a term I seldom hear in the Android world is View Model - but there really shouldn't be a reason for it. The View Model concept fits nicely into any MVC/MVP pattern. In this post I'll explain how you can and should split your model into two or even three layers of models.
[1] An example is the Connecting to the Network-article. View, Model and presenter-code all in one file and the inner class DownloadWebpageTask have dependency to the Activity. Same mistake is done in Parsing XML Data. You could also question if they maybe should hint at 3rd-party libraries existing that greatly simplifies both XML-parsing and HTTP communication.
[2] In addition to Framework-libraries, smaller libraries or micro-frameworks such as Dagger, Flow, Mortar, Parceler, Icepick and RxAndroid are building blocks which paved the way for MVP-frameworks.
The different model types
The View Model
In this post I'm defining the View Model somewhat loosely as the model mirroring a concrete user interface. Martin Fowler names it the Presentation Model. It differs from the Business Model in that it has filtered, merged or converted data for presentation in one specific context. From this follows that you can have multiple View Models for one Business Model, or multiple Business Models represented in one View Model. In the Microsoft MVVM pattern it is tightly coupled with the Business Model through databinding. That is not a requirement in my definition and use, but it can certainly be implemented if that is a requirement in your project.The Business Model
What I name the "Business Model" is the model representing an actual business entity. Think user, product , supplier or order in an online web shop system. In the Android code it contains strongly typed objects and business logic for working with these.The Transport Model
I'm going to introduce a third model type I name "The Transport Model". You will not always need it and even if you do you will not always have it as a separate class. The Transport Model is the model representing the business model in transport. If you have a REST service, it will be the class representing the JSON-data. I've found it useful to separate this as a standalone class as it allows me to unit test the parsing easily as well as allowing for switching the transport protocol with as little friction as possible. The pattern works very will with parser libraries such as Gson or Jackson.Example
I'm going to paint a scenario here with for a Movie Database app (heard it before?). The App fetches a list of movies with metadata from a JSON REST service and displays the list of movies in the app. Here is the JSON data:{ "v" : 1, "data" : [{ "released" : "2011-08-04", "title" : "Hell on Wheels", "category" : "TV series", "id" : "tt1699748", "stars" : "Anson Mount, Colm Meaney", "image" : [ "http://ia.media-imdb.com/images/M/MV5BMTQ5NTE5NTYzMF5BMl5BanBnXkFtZTgwOTc4OTY0MzE@._V1_.jpg", 1297, 1404 ] }, { "released" : "2014-04-02", "title" : "Hello Ladies: The Movie", "category" : "TV movie", "id" : "tt3762944", "stars" : "Stephen Merchant, Christine Woods", "image" : [ "http://ia.media-imdb.com/images/M/MV5BMTQ5MjYxMjkwOV5BMl5BanBnXkFtZTgwODE3MjY0MzE@._V1_.jpg", 1012, 1500 ] } ] }So this with this JSON blob i want to illustrate a typical issue: The data itself is wrapped in a somewhat unnecessary object (with the version identifier). You don't want this and other unnecessary data to bleed into your app business model. Also you want the date, image and list of stars as strongly typed data objects - so you create a set of Transport Models separate from the Business Model:
public class MovieJsonModel { public String released; public String title; public String category; public String id; public String stars; public ArrayList<Object> image; } public class MovieListJsonModel { String v; // Version public ArrayList<MovieJsonModel> data; }The classes above can be directly parsed from the JSON feed using a one-liner with Gson:
MovieListJsonModel transportModel = new GsonBuilder().fromJson(jsonString, MovieListJsonModel.class);
The business model would look somewhat different:
public class MovieModel { public String title; public String id; public Uri imageUri; public Date releaseDate; public ArrayList<String> stars; // Just illustrating that the model can and should contain business logic, // not only data. public boolean isReleased() { return releaseDate != null && releaseDate.before(new Date()); } }(To create a list of this simply instantiate an
ArrayList<MovieModel>
.)So here you see that many of the String-fields in the Transport Model have gotten a strong typed counterpart. The Date, The image URI and the comma separated list of stars is actually an
ArrayList
. I have not included the conversion code here but I'd advice to put it in the Transport Model class. This way you will not introduce a dependency from the Business Model to the transport protocol.Finally an example of how a View Model could look:
public class MovieViewModel { public String title; public String releaseDate; public Bitmap image; public int backgroundColor; public boolean isSelected; }Here you can see that String-representation of the Date again. We want to control how a Java Date object is presented, and we do that when we create the View Model. Note that this also can be unit tested. Furthermore we have a Bitmap object for the image which earlier was represented with a URL. This of course requires us to actually load the image. I'm not so determined on where to place this loading code - either in the View Model or in the Business Model.
Finally there may be fields exclusively for the view, such as the
backgroundColor
here, or state properties such as isSelected
. You may also have data from other Business Models, but I've not included that in this example.Persisting the state
One of the key benefits of having a specialized View Model is when you're persisting the model for state preservation. You probably know that you need to do this in case the system kills the process or if you rotate the device. The Parceler framework makes this simple, but we do need to make some adjustments to the simplified example above to accommodate for the non-parcelable Bitmap object:@Parcel public class MovieViewModel { public String title; public String releaseDate; @Transient public Bitmap image; public int backgroundColor; public boolean isSelected; // Reference properties: public String imageUrl; // A service client for downloading images asynchronously w/Rx private ImageService mImageService; }Now I've included the image URL as a String as well (Uri is not parcelable either). The code to load the image could be like this, using RxAndroid for callbacks (could also be implemented as a standard callback):
public Observable<Void> loadDataAsync() { if (image != null) return Observable.from(new Void[0]); else { Observable<Bitmap> imageObs = mImageService.loadImageAsync(movieModel.imageUri); Observable<Void> doneObs = imageObs .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .map(new Func1<Bitmap, Void>() { @Override public Void call(Bitmap bitmap) { image = bitmap; return null; } }); return doneObs; } }The View, Fragment or Activity would then ensure the data is always loaded as illustrated in the
onResume
method here, while the onCreate
ensure the View Model object is always instantiated:public class MovieActivity private MovieViewModel mViewModel; @Override public void onCreate() { super.onCreate(Bundle savedInstanceState); if (savedInstanceState != null) { mViewModel = (MovieViewModel) savedInstanceState.getParcelable("ViewModel"); } } @Override public void onSaveInstanceState(Bundle outState) { outState.putParcelable("ViewModel", mViewModel); super.onSaveInstanceState(outState); } @Override public void onResume() { super.onResume(); if (mViewModel != null) { loadAndBindViewModel(); } else { // Load the models asynchronously from the REST Service, // then call loadAndBindViewModel() } } private void loadAndBindViewModel() { mViewModel.loadDataAsync().subscribe(new Action1<Void>() { @Override public void call(Void dummy) { modelToUi(); } }); } private void modelToUi() { // Map all View Model properties to actual view controls }There might be cases where you want to persist the Business Model as well (or instead), like if you're doing local caching of the service data. In this case you probably do not want to store it in the Bundle attached to the Activity or Fragment though, but instead use Shared Preferences, Internal Storage or SqLite. I will not go into details about that in this post.
This comment has been removed by a blog administrator.
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDelete