{"action":"created","comment":{"url":"https://api.github.com/repos/upday/upday.github.io/pulls/comments/79152100","id":79152100,"diff_hunk":"@@ -0,0 +1,127 @@\n+---\n+layout: post\n+title: \"Model-View-ViewModel Pattern in Android\"\n+description: Our choice for the upday app - the Model-View-ViewModel pattern.\n+modified:\n+categories: blog\n+author: florina_muntenescu\n+excerpt: Our choice for the upday app - the Model-View-ViewModel pattern. Find out what it is, how we applied it in Android and why we consider it perfect for us.\n+tags: [Android, Architecture, MVVM]\n+date: 2016-09-04T15:39:55-04:00\n+---\n+After four different designs in the first six months of the development of the upday app, we learned one important lesson: we need an architecture pattern that allows fast reaction to design changes! The solution we chose in the end was Model-View-ViewModel. Discover with me what MVVM is; how we are applying it at upday and what makes it so perfect for us.\n+\n+## The Model-View-ViewModel Pattern\n+\n+The main players in the MVVM pattern are:\n+\n+* The *View* - that informs the ViewModel about the user's actions\n+* The *ViewModel* - exposes streams of data relevant to the View\n+* The *DataModel* - abstracts the data source. The ViewModel works with the DataModel to get and save the data.  \n+\n+At a first glance, MVVM seems very similar to the Model-View-Presenter pattern, because both of them do a great job in abstracting the view's state and behavior. The Presentation Model abstracts a view independent from a specific user-interface platform whereas the MVVM pattern was created by Microsoft to simplify the **event driven** programming of user interfaces.\n+\n+If, in the MVP pattern, the Presenter was telling the View directly what to display, in MVVM, **ViewModel exposes streams of events** to which the Views can bind to. Also, the Views notify the ViewModel about different actions. Thus, the MVVM pattern supports two-way data binding between the View and ViewModel and there is a many-to-one relationship between View and ViewModel. View has a reference to ViewModel but **ViewModel has no information about the View**.\n+\n+<center>\n+<picture>\n+\t<a href=\"/images/blog/model_view_viewmodel/mvvm.png\"><img src=\"/images/blog/model_view_viewmodel/mvvm.png\" alt=\"Model-View-ViewModel Architecture\"></a>\n+\t<figcaption>Model-View-ViewModel class structure</figcaption>\n+</picture>\n+</center>\n+\n+## Model-View-ViewModel at upday\n+\n+A quick look at the Android posts on the upday blog will instantly reveal what our favorite library is: RxJava. So it’s no wonder that RxJava is the backbone of upday's code. The event driven part required by MVVM is done using RxJava’s `Observable`s. Here's how we apply MVVM at upday, with the help of RxJava:\n+\n+### DataModel\n+\n+The DataModel exposes data easily consumable through event streams - RxJava's `Observable`s. It composes data from multiple sources, like the network layer, database or shared preferences and exposes them to whoever needs it. The DataModels hold the entire business logic.\n+\n+Our strong emphasis on the single responsibility principle leads to creating a DataModel for every feature in the app. For example, we have an ArticleDataModel that contains references to the API service and to the database layer. This DataModel handles the business logic ensuring that the latest news from the database is retrieved, by applying an age filter.\n+\n+### ViewModel\n+\n+The ViewModel is a model for the View of the app, an abstraction of the View. The ViewModel retrieves from the DataModel the necessary data, applies the UI logic and then exposes relevant data for the View to consume, together with behaviors for the View. Similar to the DataModel, the ViewModel exposes the data via `Observable`s.\n+\n+We learned two things about the ViewModel the hard way:\n+\n+* The ViewModel should expose states for the View, rather than just events. For example, if we need to display the name and the email address of a `User`, rather than creating two streams for this, we create a `DisplayableUser` object that encapsulates the two fields. The stream will emit every time the display name or the email change. Like this, we ensure that our View always displays the current state of the `User`.\n+\n+* We should make sure that every action of the user goes through the ViewModel and that any possible logic of the View is moved in the ViewModel.\n+\n+We shared more details about these two topics in a blog post about <a href=\"https://upday.github.io/mvvm_rx_common_mistakes\">common mistakes in MVVM + RxJava</a>.  \n+\n+### View\n+\n+The View is the actual user interface in the app. It can be an `Activity`, a `Fragment` or any custom Android `View`. For `Activities` and `Fragment`s, we are binding and unbinding from the event sources on `onResume()` and `onPause()`.\n+\n+{% highlight java %}\n+    private final CompositeSubscription mSubscription = new CompositeSubscription();\n+\n+    @Override\n+    public void onResume() {\n+        super.onResume();\n+        mSubscription.add(mViewModel.getSomeData()\n+                         .observeOn(AndroidSchedulers.mainThread())\n+                         .subscribe(this::updateView,\n+                                    this::handleError));\n+    }\n+\n+    @Override\n+    public void onPause() {\n+        mSubscription.clear();\n+        super.onPause();\n+    }\n+{% endhighlight %}\n+\n+In case the MVVM View is a custom Android `View`, the binding is done in the constructor. To ensure that the subscription is not preserved, leading to possible memory leaks, the unbinding happens in `onDetachedFromWindow`.\n+\n+{% highlight java %}\n+    private final CompositeSubscription mSubscription = new CompositeSubscription();\n+\n+    public MyView(Context context, MyViewModel viewModel) {\n+        ...\n+        mSubscription.add(mViewModel.getSomeData()\n+                         .observeOn(AndroidSchedulers.mainThread())\n+                         .subscribe(this::updateView,\n+                                    this::handleError));\n+    }\n+\n+    @Override\n+    public void onDetachedFromWindow() {\n+        mSubscription.clear();\n+        super.onDetachedFromWindow();\n+    }\n+}\n+{% endhighlight %}\n+\n+## Testability Of The Model-View-ViewModel Classes\n+\n+One of the main reasons why we love the Model-View-ViewModel pattern is its high degree of testability.\n+\n+### DataModel\n+\n+The inversion of control pattern, heavily applied in our code, and the lack of any Android classes, make the DataModel easy to unit test.\n+\n+### ViewModel\n+\n+The ViewModel exposes streams of data to whoever needs to consume it. We see the Views and the unit tests as two different types of data consumers.\n+The ViewModel is completely separated from the UI or any Android classes, therefore straightforward to unit test.\n+If the ViewModel needs access to Android classes, we create wrappers that we call `Provider`s. For example, for resources we created a `IResourceProvider`, that exposes methods like `String getString(@StringRes final int id)`. The implementation of the `IResourceProvider` will contain a reference to the `Context` but, the ViewModel will only get a reference to a `IResourceProvider` injected.\n+\n+### View\n+\n+Given that the logic in the UI is minimal, the Views are easy to test with Espresso. We are also using libraries like DaggerMock and MockWebServer to help us test the UI of our app.\n+\n+## Is MVVM The Right Solution?  \n+\n+We have been using MVVM together with RxJava for almost a year now. We have seen that since the View is just a consumer of the ViewModel, it was easy to just replace different UI elements, with minimal, or sometimes zero changes in other classes.\n+\n+We have also learnt how important separation of concerns is and that we should split the code more, creating small Views and small ViewModels that only have specific responsibilities. Like this, when our UI requirements changed once more, we could just replace some UI elements with new ones, with little changes in the Views that contain the small ones.  ","path":"_posts/blog/2016-09-06-model-view-viewmodel.md","position":121,"original_position":121,"commit_id":"0881bfaa9dd034c554e6fed742202876c2dace56","original_commit_id":"0881bfaa9dd034c554e6fed742202876c2dace56","user":{"login":"pkrauss-asideas","id":6370330,"avatar_url":"https://avatars.githubusercontent.com/u/6370330?v=3","gravatar_id":"","url":"https://api.github.com/users/pkrauss-asideas","html_url":"https://github.com/pkrauss-asideas","followers_url":"https://api.github.com/users/pkrauss-asideas/followers","following_url":"https://api.github.com/users/pkrauss-asideas/following{/other_user}","gists_url":"https://api.github.com/users/pkrauss-asideas/gists{/gist_id}","starred_url":"https://api.github.com/users/pkrauss-asideas/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pkrauss-asideas/subscriptions","organizations_url":"https://api.github.com/users/pkrauss-asideas/orgs","repos_url":"https://api.github.com/users/pkrauss-asideas/repos","events_url":"https://api.github.com/users/pkrauss-asideas/events{/privacy}","received_events_url":"https://api.github.com/users/pkrauss-asideas/received_events","type":"User","site_admin":false},"body":"...the small ones.","created_at":"2016-09-16T11:39:15Z","updated_at":"2016-09-16T11:39:15Z","html_url":"https://github.com/upday/upday.github.io/pull/27#discussion_r79152100","pull_request_url":"https://api.github.com/repos/upday/upday.github.io/pulls/27","_links":{"self":{"href":"https://api.github.com/repos/upday/upday.github.io/pulls/comments/79152100"},"html":{"href":"https://github.com/upday/upday.github.io/pull/27#discussion_r79152100"},"pull_request":{"href":"https://api.github.com/repos/upday/upday.github.io/pulls/27"}}},"pull_request":{"url":"https://api.github.com/repos/upday/upday.github.io/pulls/27","id":85458517,"html_url":"https://github.com/upday/upday.github.io/pull/27","diff_url":"https://github.com/upday/upday.github.io/pull/27.diff","patch_url":"https://github.com/upday/upday.github.io/pull/27.patch","issue_url":"https://api.github.com/repos/upday/upday.github.io/issues/27","number":27,"state":"open","locked":false,"title":"MVVM","user":{"login":"florina-muntenescu","id":2998890,"avatar_url":"https://avatars.githubusercontent.com/u/2998890?v=3","gravatar_id":"","url":"https://api.github.com/users/florina-muntenescu","html_url":"https://github.com/florina-muntenescu","followers_url":"https://api.github.com/users/florina-muntenescu/followers","following_url":"https://api.github.com/users/florina-muntenescu/following{/other_user}","gists_url":"https://api.github.com/users/florina-muntenescu/gists{/gist_id}","starred_url":"https://api.github.com/users/florina-muntenescu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/florina-muntenescu/subscriptions","organizations_url":"https://api.github.com/users/florina-muntenescu/orgs","repos_url":"https://api.github.com/users/florina-muntenescu/repos","events_url":"https://api.github.com/users/florina-muntenescu/events{/privacy}","received_events_url":"https://api.github.com/users/florina-muntenescu/received_events","type":"User","site_admin":false},"body":"","created_at":"2016-09-15T18:46:36Z","updated_at":"2016-09-16T11:39:15Z","closed_at":null,"merged_at":null,"merge_commit_sha":"8b1c276dd08d3709db30242f45e8198bc43c82ae","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/upday/upday.github.io/pulls/27/commits","review_comments_url":"https://api.github.com/repos/upday/upday.github.io/pulls/27/comments","review_comment_url":"https://api.github.com/repos/upday/upday.github.io/pulls/comments{/number}","comments_url":"https://api.github.com/repos/upday/upday.github.io/issues/27/comments","statuses_url":"https://api.github.com/repos/upday/upday.github.io/statuses/0881bfaa9dd034c554e6fed742202876c2dace56","head":{"label":"upday:mvvm","ref":"mvvm","sha":"0881bfaa9dd034c554e6fed742202876c2dace56","user":{"login":"upday","id":16613366,"avatar_url":"https://avatars.githubusercontent.com/u/16613366?v=3","gravatar_id":"","url":"https://api.github.com/users/upday","html_url":"https://github.com/upday","followers_url":"https://api.github.com/users/upday/followers","following_url":"https://api.github.com/users/upday/following{/other_user}","gists_url":"https://api.github.com/users/upday/gists{/gist_id}","starred_url":"https://api.github.com/users/upday/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/upday/subscriptions","organizations_url":"https://api.github.com/users/upday/orgs","repos_url":"https://api.github.com/users/upday/repos","events_url":"https://api.github.com/users/upday/events{/privacy}","received_events_url":"https://api.github.com/users/upday/received_events","type":"Organization","site_admin":false},"repo":{"id":53958722,"name":"upday.github.io","full_name":"upday/upday.github.io","owner":{"login":"upday","id":16613366,"avatar_url":"https://avatars.githubusercontent.com/u/16613366?v=3","gravatar_id":"","url":"https://api.github.com/users/upday","html_url":"https://github.com/upday","followers_url":"https://api.github.com/users/upday/followers","following_url":"https://api.github.com/users/upday/following{/other_user}","gists_url":"https://api.github.com/users/upday/gists{/gist_id}","starred_url":"https://api.github.com/users/upday/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/upday/subscriptions","organizations_url":"https://api.github.com/users/upday/orgs","repos_url":"https://api.github.com/users/upday/repos","events_url":"https://api.github.com/users/upday/events{/privacy}","received_events_url":"https://api.github.com/users/upday/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/upday/upday.github.io","description":"","fork":false,"url":"https://api.github.com/repos/upday/upday.github.io","forks_url":"https://api.github.com/repos/upday/upday.github.io/forks","keys_url":"https://api.github.com/repos/upday/upday.github.io/keys{/key_id}","collaborators_url":"https://api.github.com/repos/upday/upday.github.io/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/upday/upday.github.io/teams","hooks_url":"https://api.github.com/repos/upday/upday.github.io/hooks","issue_events_url":"https://api.github.com/repos/upday/upday.github.io/issues/events{/number}","events_url":"https://api.github.com/repos/upday/upday.github.io/events","assignees_url":"https://api.github.com/repos/upday/upday.github.io/assignees{/user}","branches_url":"https://api.github.com/repos/upday/upday.github.io/branches{/branch}","tags_url":"https://api.github.com/repos/upday/upday.github.io/tags","blobs_url":"https://api.github.com/repos/upday/upday.github.io/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/upday/upday.github.io/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/upday/upday.github.io/git/refs{/sha}","trees_url":"https://api.github.com/repos/upday/upday.github.io/git/trees{/sha}","statuses_url":"https://api.github.com/repos/upday/upday.github.io/statuses/{sha}","languages_url":"https://api.github.com/repos/upday/upday.github.io/languages","stargazers_url":"https://api.github.com/repos/upday/upday.github.io/stargazers","contributors_url":"https://api.github.com/repos/upday/upday.github.io/contributors","subscribers_url":"https://api.github.com/repos/upday/upday.github.io/subscribers","subscription_url":"https://api.github.com/repos/upday/upday.github.io/subscription","commits_url":"https://api.github.com/repos/upday/upday.github.io/commits{/sha}","git_commits_url":"https://api.github.com/repos/upday/upday.github.io/git/commits{/sha}","comments_url":"https://api.github.com/repos/upday/upday.github.io/comments{/number}","issue_comment_url":"https://api.github.com/repos/upday/upday.github.io/issues/comments{/number}","contents_url":"https://api.github.com/repos/upday/upday.github.io/contents/{+path}","compare_url":"https://api.github.com/repos/upday/upday.github.io/compare/{base}...{head}","merges_url":"https://api.github.com/repos/upday/upday.github.io/merges","archive_url":"https://api.github.com/repos/upday/upday.github.io/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/upday/upday.github.io/downloads","issues_url":"https://api.github.com/repos/upday/upday.github.io/issues{/number}","pulls_url":"https://api.github.com/repos/upday/upday.github.io/pulls{/number}","milestones_url":"https://api.github.com/repos/upday/upday.github.io/milestones{/number}","notifications_url":"https://api.github.com/repos/upday/upday.github.io/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/upday/upday.github.io/labels{/name}","releases_url":"https://api.github.com/repos/upday/upday.github.io/releases{/id}","deployments_url":"https://api.github.com/repos/upday/upday.github.io/deployments","created_at":"2016-03-15T16:02:24Z","updated_at":"2016-09-01T16:42:19Z","pushed_at":"2016-09-16T10:30:27Z","git_url":"git://github.com/upday/upday.github.io.git","ssh_url":"git@github.com:upday/upday.github.io.git","clone_url":"https://github.com/upday/upday.github.io.git","svn_url":"https://github.com/upday/upday.github.io","homepage":null,"size":23572,"stargazers_count":6,"watchers_count":6,"language":"CSS","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":1,"mirror_url":null,"open_issues_count":3,"forks":1,"open_issues":3,"watchers":6,"default_branch":"master"}},"base":{"label":"upday:master","ref":"master","sha":"320bd538f3bff4ab6ea89ffd56a06ebf2cdcb757","user":{"login":"upday","id":16613366,"avatar_url":"https://avatars.githubusercontent.com/u/16613366?v=3","gravatar_id":"","url":"https://api.github.com/users/upday","html_url":"https://github.com/upday","followers_url":"https://api.github.com/users/upday/followers","following_url":"https://api.github.com/users/upday/following{/other_user}","gists_url":"https://api.github.com/users/upday/gists{/gist_id}","starred_url":"https://api.github.com/users/upday/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/upday/subscriptions","organizations_url":"https://api.github.com/users/upday/orgs","repos_url":"https://api.github.com/users/upday/repos","events_url":"https://api.github.com/users/upday/events{/privacy}","received_events_url":"https://api.github.com/users/upday/received_events","type":"Organization","site_admin":false},"repo":{"id":53958722,"name":"upday.github.io","full_name":"upday/upday.github.io","owner":{"login":"upday","id":16613366,"avatar_url":"https://avatars.githubusercontent.com/u/16613366?v=3","gravatar_id":"","url":"https://api.github.com/users/upday","html_url":"https://github.com/upday","followers_url":"https://api.github.com/users/upday/followers","following_url":"https://api.github.com/users/upday/following{/other_user}","gists_url":"https://api.github.com/users/upday/gists{/gist_id}","starred_url":"https://api.github.com/users/upday/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/upday/subscriptions","organizations_url":"https://api.github.com/users/upday/orgs","repos_url":"https://api.github.com/users/upday/repos","events_url":"https://api.github.com/users/upday/events{/privacy}","received_events_url":"https://api.github.com/users/upday/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/upday/upday.github.io","description":"","fork":false,"url":"https://api.github.com/repos/upday/upday.github.io","forks_url":"https://api.github.com/repos/upday/upday.github.io/forks","keys_url":"https://api.github.com/repos/upday/upday.github.io/keys{/key_id}","collaborators_url":"https://api.github.com/repos/upday/upday.github.io/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/upday/upday.github.io/teams","hooks_url":"https://api.github.com/repos/upday/upday.github.io/hooks","issue_events_url":"https://api.github.com/repos/upday/upday.github.io/issues/events{/number}","events_url":"https://api.github.com/repos/upday/upday.github.io/events","assignees_url":"https://api.github.com/repos/upday/upday.github.io/assignees{/user}","branches_url":"https://api.github.com/repos/upday/upday.github.io/branches{/branch}","tags_url":"https://api.github.com/repos/upday/upday.github.io/tags","blobs_url":"https://api.github.com/repos/upday/upday.github.io/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/upday/upday.github.io/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/upday/upday.github.io/git/refs{/sha}","trees_url":"https://api.github.com/repos/upday/upday.github.io/git/trees{/sha}","statuses_url":"https://api.github.com/repos/upday/upday.github.io/statuses/{sha}","languages_url":"https://api.github.com/repos/upday/upday.github.io/languages","stargazers_url":"https://api.github.com/repos/upday/upday.github.io/stargazers","contributors_url":"https://api.github.com/repos/upday/upday.github.io/contributors","subscribers_url":"https://api.github.com/repos/upday/upday.github.io/subscribers","subscription_url":"https://api.github.com/repos/upday/upday.github.io/subscription","commits_url":"https://api.github.com/repos/upday/upday.github.io/commits{/sha}","git_commits_url":"https://api.github.com/repos/upday/upday.github.io/git/commits{/sha}","comments_url":"https://api.github.com/repos/upday/upday.github.io/comments{/number}","issue_comment_url":"https://api.github.com/repos/upday/upday.github.io/issues/comments{/number}","contents_url":"https://api.github.com/repos/upday/upday.github.io/contents/{+path}","compare_url":"https://api.github.com/repos/upday/upday.github.io/compare/{base}...{head}","merges_url":"https://api.github.com/repos/upday/upday.github.io/merges","archive_url":"https://api.github.com/repos/upday/upday.github.io/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/upday/upday.github.io/downloads","issues_url":"https://api.github.com/repos/upday/upday.github.io/issues{/number}","pulls_url":"https://api.github.com/repos/upday/upday.github.io/pulls{/number}","milestones_url":"https://api.github.com/repos/upday/upday.github.io/milestones{/number}","notifications_url":"https://api.github.com/repos/upday/upday.github.io/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/upday/upday.github.io/labels{/name}","releases_url":"https://api.github.com/repos/upday/upday.github.io/releases{/id}","deployments_url":"https://api.github.com/repos/upday/upday.github.io/deployments","created_at":"2016-03-15T16:02:24Z","updated_at":"2016-09-01T16:42:19Z","pushed_at":"2016-09-16T10:30:27Z","git_url":"git://github.com/upday/upday.github.io.git","ssh_url":"git@github.com:upday/upday.github.io.git","clone_url":"https://github.com/upday/upday.github.io.git","svn_url":"https://github.com/upday/upday.github.io","homepage":null,"size":23572,"stargazers_count":6,"watchers_count":6,"language":"CSS","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":1,"mirror_url":null,"open_issues_count":3,"forks":1,"open_issues":3,"watchers":6,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/upday/upday.github.io/pulls/27"},"html":{"href":"https://github.com/upday/upday.github.io/pull/27"},"issue":{"href":"https://api.github.com/repos/upday/upday.github.io/issues/27"},"comments":{"href":"https://api.github.com/repos/upday/upday.github.io/issues/27/comments"},"review_comments":{"href":"https://api.github.com/repos/upday/upday.github.io/pulls/27/comments"},"review_comment":{"href":"https://api.github.com/repos/upday/upday.github.io/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/upday/upday.github.io/pulls/27/commits"},"statuses":{"href":"https://api.github.com/repos/upday/upday.github.io/statuses/0881bfaa9dd034c554e6fed742202876c2dace56"}}}}
{"actor":{"display_login":"pkrauss-asideas"}}