In this post we will see how usage of ViewModel Architecture Component can actually degrade the quality of Android projects.

ViewModel:

ViewModel class and the general feature associated with it are parts of Android Architecture Components – a set of experimental features for Android that were announced by Google at Google IO 2017 conference.

On the surface, the purpose of ViewModel looks very legitimate – it can be used in order to “tie” data to the logical scope of either Activity or Fragment.

The scope being logical means that it remains the same even if e.g. Activity is destroyed and then re-created upon rotation. This makes it easy to retain data in Activities and Fragments upon configuration changes. Sure thing we all need this feature! Like, yesterday!

Comparison of ViewModel to Manual Save and Restore Implementation:

But let’s take a step back for a moment. How did we retain data in e.g. Activity upon configuration change until now? The following code snippet demonstrates the “old” approach:

public class OldApproachActivity extends Activity {

    private static final String USER_ID = "USER_ID";
    private static final String PRODUCT_ID = "PRODUCT_ID";

    private String mUserId;
    private String mProductId;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState != null) {
            mUserId = savedInstanceState.getString(USER_ID);
            mProductId = savedInstanceState.getString(PRODUCT_ID);
        }
        ...
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(USER_ID, mUserId);
        outState.putString(PRODUCT_ID, mProductId);
    }

    ...
}

For comparison, retaining the same variables during configuration change using ViewModel could be done like this:

public class ViewModelActivity extends Activity {

    private MyViewModel mViewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        ...
    }
    
    ...

    public static class MyViewModel extends ViewModel {
        private String mUserId;
        private String mProductId;
    }

}

If we would need to decide which approach is better based on the simple samples above, it is only natural to choose the one involving ViewModel. It is shorter and clearer.

The no-so-evident gotcha, however, is that the above two code snippets are not functionally equivalent!

During “normal” interaction of the user with the application both approaches produce the same result, but the “old” approach also supports “save and restore” flow, whereas the one involving ViewModel doesn’t.

Save and Restore Flow:

As described in Process and Application Life Cycle, application’s process can be killed by Android at any time due to low memory conditions. Using this mechanism Android can reclaim the resources consumed by applications when it needs them.

In most cases, Android will notify the process that it is about to be killed, thus giving it a chance to save application’s state. Then, when the application is started again (either due to user interaction or automatically), it will be given an opportunity to restore its previous state. This flow is called “save and restore”.

Let’s review one specific example.

Suppose that the application have a back stack of Activities when Android decides to kill it.

Before killing the application, Android will call onSaveInstanceState(Bundle) method of each Activity on the back stack. Developers need to use the provided Bundle in order to persist data that will allow to restore the state of each Activity in the future.

In addition, Android will automatically save the state of the back stack itself (i.e. which Activities are shown and their ordering).

When the application is restarted, Android will automatically restore the previous state of the back stack. User can then navigate back to all Activities that had been present on the back stack when the application was killed, and Android will automatically recreate them.

When Activities from the back stack are being recreated, Android passes the respective saved data to onCreate(Bundle) methods. Developers need to use the data persisted in the Bundle in order to restore the state of each Activity.

The process of save and restore of each Activity can be roughly summarized as follows:

android_save_restore

Correct implementation of “save and restore” flow allows the user to have a fluent experience that is not affected by Android’s memory management activities.

If you go back and read the code demonstrating the “old” approach above, you will notice that in addition to supporting configuration changes, it also supports save and restore flow. In case of the new approach involving ViewModel, however, save and restore flow results in loss of developer managed state.

Note that I wrote “developer managed state”. Android will always do its part and restore the state it is responsible for. It is only the state kept in ViewModel that will not be restored.

This state inconsistency is much worse than not restoring any state at all. Imagine that the user can navigate back to previous Activities, but their state was restored only partially (e.g. some references that must not be nullable during normal operation are nulls).

In such a case, you will be lucky if application just crashes with NPE. Then, at least, the user will know that something bad happened and have a chance to start application from scratch. Much worse if the application does not crash, but the user is stuck with e.g. infinite progress indication or inconsistent UI. The worst of all scenarios is if the application seems to be working, but silently corrupts user’s data.

The video below demonstrates a bug related to save and restore flow that I found in the new StackOverflow application for Android:

Keep in mind that this is the least dangerous of all the save and restore bugs – just user interface inconsistency, followed by application’s crash.

Why Care About Save and Restore Flow:

Some developers argue that save and restore is such a rare scenario, that it can be ignored until proven otherwise.

Other developers don’t go as far as ignoring save and restore, but still say that if it occurs then it means that the user haven’t interacted with the application for a long time. In this case, they say, we can just assume that previous state is outdated anyway and restart the application from scratch.

You might’ve heard additional forms of this argument. The assumption underlying all these claims is that save and restore does not occur very often.

The reality, however, is that save and restore flow is being constantly experienced by majority of your users.

It is true that people who own modern flagship devices with huge amounts of RAM couldn’t care less about save and restore, but I experience save and restore on a daily basis on my personal Samsung Galaxy S4 having 2GB of RAM.

Take a look at this chart from AppBrain showing the most used Android phones as of May 2017:

Most used Android phones

Note how your average user is more likely to have Samsung Galaxy S3 than Samsung Galaxy S7 Edge. Also note that the second most used phone is Samsung Galaxy Grand Prime which has just 1GB of RAM! Given that Samsung phones are on the high performance side, users of other phones are most likely to have similar or even smaller amounts of RAM.

Think about it – a huge amount of your users (probably above 50%, unless you have a niche market) own phones that have less than 2GB of RAM.

On my Samsung Galaxy S4, which has 2GB of RAM, Android starts killing applications after I have approximately 10 applications open in background. Your average user will have more than that.

Therefore, handling of save and restore flow is not optional. Professional Android developers who care about their users must implement a first class support for save and restore in their applications.

Equipped with this knowledge we can get back to architecture components.

Usefulness of ViewModel:

In the previous section we saw that support for save and restore flow is not optional and must always be implemented. Earlier we saw that implementation of save and restore also handles configuration changes. So why would we want to use ViewModel then?

There is one use case for ViewModel that was stated by many developers. Since it seems to be so resonating with the community, let’s discuss it in details.

Turns out that there is an upper limit on the amount of data that can be stored during save and restore flow. I couldn’t find the exact number, but looks like it is at approximately 500kB. If you attempt to save more than 500kB of data during onSaveInstanceState() call, you will be presented with TransactionTooLargeException.

In this situation it becomes very tempting to use ViewModel in order to retain big data sets during configuration changes.

In my opinion, under no circumstances should large data sets be “stored” in either Activities or Fragments. Let me explain why using two imaginary scenarios.

Imagine that the user interacts with Activity and fetches 1MB of data from the web. If all this data is stored in Activity then when the user clicks “back” button and navigates to the previous screen this data is lost. If at this point the user decides to open the same Activity again, then all the data will need to be re-fetched from the network. This network call duplication due to a simple back-and-forth between screens is not a very good design.

Now imagine that we implement search functionality. In this case, even if the user searches for the same string, we must still fetch a new data from the network upon each search request. We are not concerned with back-and-forth between screens anymore because we hit the network anyway. It might seem that storing search results in Activity is a valid design choice in this case, but imagine that you discover at some point that you need the search results in other part of the application. The fact that search results are stored in Activity becomes an obstacle because there is no reliable way to access them outside of this Activity.

The above examples show that while ViewModel can be used in order to retain large data sets upon rotation, it is probably a bad design choice to start with. The fact that it can be done doesn’t imply that it should be done.

The only state that should be stored in Activities and Fragments is UI state and data identifiers.

Large data sets should be stored in Application scope, or, depending on the requirements, persisted to SQLite or some other persistent storage.

Since there is no clear valid use case for ViewModel Architecture Component, its introduction can be hardly justified.

ViewModel Architecture Component Considered Harmful:

In the previous section we discussed why ViewModel is somewhat useless addition to developer’s toolbox. However, I think that it is not just useless, but harmful.

As I said earlier, many developers (especially inexperienced ones) don’t realize the importance of proper save and restore support. But even when save and restore support is implemented, it is rarely included in the testing plan.

[There is a widespread myth that testing save and restore is difficult because there is no easy way to cause the process to be killed in background. On my phone it is very simple: go to Developer Options and set “limit background processes” option to 1.]

While save and restore support is often neglected and rarely tested (in my experience; YMMV), configuration changes support is commonly implemented and tested. In fact, any application that is not locked to portrait mode will be thoroughly tested in context of configuration changes.

Until today, the fact that support for configuration changes also provided support for most parts of save and restore flow (unless some exotic techniques were used) mitigated the problem a bit. And even when bugs with save and restore were found, they could be fixed while keeping the general design intact.

Introduction of ViewModel changes the situation cardinally. Now developers will be able to implement support for configuration changes while completely ignoring save and restore flow.

It doesn’t take too much imagination to see that the following two step scenario will take place on many projects:

  1. Application is released without save and restore support. Users are being bombarded by save and restore bugs.
  2. Upon realization of the problem, developers “hack” save and restore support alongside ViewModel approach.

The immediate result of lack of save and restore support will be user facing bugs. Some of them might be as innocent as NPE crashes, others might silently corrupt users data.

Assuming best case scenario of application crashes, developers will be able to quickly identify the issues using various “crashlytics” tools. If the application will just hung or silently corrupt user’s data, then developers will need to investigate the problem based on 1-star user reviews in Google Play.

Once the root cause of the bugs will be identified, save and restore support will need to be implemented. However, it is not likely that developers will refactor the applications in order to get rid of ViewModel – at this point it will be too risky and time consuming. Therefore, save and restore support will be just hacked on top of the existing design. The result of these hacks will be long term maintainability issues and more bugs.

And there is more to it. Since it is so simple to retain data in Activities and Fragments using ViewModel approach, the average amount of memory consumed by applications will increase. Even if different components will need access to the same data, it will be very easy to have each of them to retain its own dataset.

Applications will consume more memory, which will lead to more frequent killing of processes by Android, which will lead to the bugs in save and restore to affect even more users.

Summary:

In this post we saw how important it is to handle save and restore flow and how introduction of ViewModel can negatively impact the users of our applications. Especially the users who own older or lower end devices.

If you found this post useful or interesting, please share it with you colleagues in order to let them know about the risks associated with ViewModel Architecture Component, and consider subscribing to our newsletter in order to receive notifications about new posts.

This article has 6 comments

  1. Riley Ackerman Reply

    ViewModel is only useless if you use it to persist state of small objects. If you are holding very large objects in memory (anything 0.5 MB or greater), then it is to persist state across configuration changes as it helps avoid the TransactionTooLargeException.

    An ideal implementation would be to save an identifier of large objects in instance state and use ViewModel to persist the object across rotations. In this manner, we get fast and cheap object persistence across configuration changes, but still have the ability to recover from process/activity death.

    • Vasiliy Reply

      I would argue that TransactionTooLargeException is a hint that it is time to have a critical look at your design, rather than a sign that ViewModel is required.

      Let’s say you use ViewModel like you said. If user navigates back and then opens the same activity again, the data will not be there. So, if ViewModel was the only mechanism you used in order to store 0.5MB of data – all this data will need to be re-fetched and re-processed just because the user made a simple back-and-forth navigation between screens. This design is not optimal and should be reviewed.

  2. Tony Reply

    One other benefit of ViewModels not outlined here is transient/non-config instances, e.g. Threads or network requests.

    I agree that using ViewModels might cause devs to overlook Config saving, and that is something that needs to be more explicitly addressed in the Arch Components. However, ViewModels in this paradigm allows for better code fluidity between Activity instances. VMs allow holding on to necessary & long-lived transient objects without hot-swapping callbacks or incorporating a temp cache strategy to handle config-changes appropriately.

    • Vasiliy Reply

      Hi Tony, thanks for your comment.
      You’re right that ViewModel can allow for relatively easy “sharing” of e.g. network requests between instances of the same Activity on config changes. However, in order to do this ViewModel will need to have a reference to these network requests.
      This might not be evident immediately, but if you try to implement this approach in code you will notice that parts of business logic start leaking into ViewModel. The more such parts you’ll have, the more you will contaminate the ViewModel with logic.
      Such a contamination is not in line with the main purpose of MVVM: separation between business and presentation logic. It is, in essence, just a quick and dirty hack that will have negative impact on maintainability of your project. Therefore, while what you say can be done, I doubt that it should be done.
      What’s your alternatives? From the top of my head:

      1) Encapsulate network requests in classes that “live” in Application scope.
      2) Encapsulate network requests in Services
      3) Encapsulate network requests in Sync Adapter, JobScheduler, etc.

      All of these alternatives require more work, but this is a classical engineer’s dilemma: do it quick and dirty, or do it right. In my experience, quick and dirty is never a good idea.

  3. Pa Reply

    Hi,

    I do not understand the comparison between ViewModel and the “Save and Restore flow”. Simply put, IMHO these facilities are meant to be used in different contexts. Before ViewModel was there an easy way to preserve state on configuration changes? It was a nightmare with retained fragments for example. It was more easy to forget to handle some lifecycle flows, propagate data between fragments and activities and so on. Not to mention other approaches… In any case propagating state between activities instances using OnSaveInstanceState was and, as you said, is necessary. I think that the ViewModel covers a huge missing part of the Android platform (not to mention the possibility to be able to observe data, share live data between fragments without worrying in almost all cases to register/deregister listeners and so on). Surely it is perfectible as everything. How would you improve/replace it?

    Thanks.

    • Vasiliy Reply

      Hi, thanks for your comment.
      You are right – ViewModel and save and restore flow are different contexts. This is exactly the major deficiency of ViewModel that I tried to show in this post.
      If you think about it conceptually, then save and restore and config changes require the same functionality – preserve data upon Activity (or Fragment) being destroyed and re-created.
      It doesn’t make sense to use two completely different approaches in order to achieve the same functionality. If you do use both (and I invite you to try to do it in a real application, not artificial examples), you will notice that it involves more effort and results in more complex design. It also gives rise to corner case scenarios related to integration between these approaches, which means that there will be more bugs.
      If you do use both, you’ll have a very bad design, but, at least, be covering both bases. My main problem with ViewModel is that it makes it very easy to forget about save and restore completely and handle only config changes. You might be an experienced developer who knows how to implement and test save and restore support, but many developers don’t. This means that users will get more save and restore bugs.
      For example of save and restore bug in StackOverflow (!) application see this video: https://youtu.be/-GAoc0hJ4_I

Leave a Comment

Your email address will not be published. Required fields are marked *