Architecture Components

Architecture Components: ViewModel

Architecture Components were announced at Google I/O 2017 and provide some frameworks to help developers create more maintainable and more robust apps. The architecture components don’t specifically do anything which will prevent apps from crashing or otherwise misbehaving, but they offer some frameworks which encourages decoupled components within the app which should lead to better behaved app. In this series we’ll take a look at these components and see how we can benefit from them. In this first article we’ll take a look at the ViewModel.

Previously we looked at both lifecycle components and LiveData and saw how they can simplify the creation of data objects, but anyone who has done more than a little bit of Android development will appreciate that often we require objects which need to live longer than the Fragment or Activity they are attached to. An obvious situation where this is important is when handling configuration changes, such as device orientation changes. By turning on Hugo logging (see this article for more information about Hugo) on the PlayServices LocationProvider class we can see what happens when we rotate the device:

06-24 13:02:49.121 V/LocationLiveData: ⇢ <init>(context=com.stylingandroid.location.services.MainActivity@d48104c)
06-24 13:02:49.123 V/LocationLiveData: ⇢ <init>(this$0=com.stylingandroid.location.services.livedata.LocationLiveData@903038b)
06-24 13:02:49.123 V/LocationLiveData: ⇠ <init> [0ms]
06-24 13:02:49.123 V/LocationLiveData: ⇠ <init> [1ms]
06-24 13:02:49.186 V/LocationLiveData: ⇢ onActive()
06-24 13:02:49.188 V/LocationLiveData: ⇢ getFusedLocationProviderClient()
06-24 13:02:49.214 V/LocationLiveData: ⇠ getFusedLocationProviderClient [25ms] = com.google.android.gms.location.FusedLocationProviderClient@69fe57b
06-24 13:02:49.222 V/LocationLiveData: ⇠ onActive [35ms]
06-24 13:02:50.287 V/LocationLiveData: ⇢ onLocationResult(locationResult=LocationResult[locations: [Location[fused 51.753902,-0.461009 acc=20 et=+15d4h19m26s161ms]]])
06-24 13:02:50.289 V/LocationLiveData: ⇢ access$000(x0=com.stylingandroid.location.services.livedata.LocationLiveData@903038b, x1=com.stylingandroid.location.services.CommonLocation@398d437)
06-24 13:02:50.297 V/LocationLiveData: ⇠ access$000 [7ms]
06-24 13:02:50.297 V/LocationLiveData: ⇠ onLocationResult [10ms]
06-24 13:03:12.474 V/LocationLiveData: ⇢ onInactive()
06-24 13:03:12.475 V/LocationLiveData: ⇠ onInactive [0ms]
06-24 13:03:12.518 V/LocationLiveData: ⇢ <init>(context=com.stylingandroid.location.services.MainActivity@128132f)
06-24 13:03:12.518 V/LocationLiveData: ⇢ <init>(this$0=com.stylingandroid.location.services.livedata.LocationLiveData@942ad3c)
06-24 13:03:12.518 V/LocationLiveData: ⇠ <init> [0ms]
06-24 13:03:12.519 V/LocationLiveData: ⇠ <init> [0ms]
06-24 13:03:12.554 V/LocationLiveData: ⇢ onActive()
06-24 13:03:12.555 V/LocationLiveData: ⇢ getFusedLocationProviderClient()
06-24 13:03:12.555 V/LocationLiveData: ⇠ getFusedLocationProviderClient [0ms] = com.google.android.gms.location.FusedLocationProviderClient@e29dd6c
06-24 13:03:12.555 V/LocationLiveData: ⇠ onActive [1ms]
06-24 13:03:12.562 V/LocationLiveData: ⇢ <init>(context=com.stylingandroid.location.services.MainActivity@128132f)
06-24 13:03:12.562 V/LocationLiveData: ⇢ <init>(this$0=com.stylingandroid.location.services.livedata.LocationLiveData@892dfca)
06-24 13:03:12.562 V/LocationLiveData: ⇠ <init> [0ms]
06-24 13:03:12.562 V/LocationLiveData: ⇠ <init> [0ms]
06-24 13:03:12.566 V/LocationLiveData: ⇢ onInactive()
06-24 13:03:12.567 V/LocationLiveData: ⇠ onInactive [0ms]
06-24 13:03:12.576 V/LocationLiveData: ⇢ onActive()
06-24 13:03:12.577 V/LocationLiveData: ⇢ getFusedLocationProviderClient()
06-24 13:03:12.577 V/LocationLiveData: ⇠ getFusedLocationProviderClient [0ms] = com.google.android.gms.location.FusedLocationProviderClient@3a4ba17
06-24 13:03:12.577 V/LocationLiveData: ⇠ onActive [1ms]
06-24 13:03:12.727 V/LocationLiveData: ⇢ onLocationResult(locationResult=LocationResult[locations: [Location[fused 51.753902,-0.461009 acc=20 et=+15d4h19m49s480ms]]])
06-24 13:03:12.727 V/LocationLiveData: ⇢ access$000(x0=com.stylingandroid.location.services.livedata.LocationLiveData@892dfca, x1=com.stylingandroid.location.services.CommonLocation@5735b1e)
06-24 13:03:12.731 V/LocationLiveData: ⇠ access$000 [3ms]
06-24 13:03:12.731 V/LocationLiveData: ⇠ onLocationResult [3ms]

The important things to note are that three different instances of LocationLiveData are created; and each time getFusedLocationProvider() is called, then a different instance is returned.

The three different instances of LocationLiveData aren’t so much of a problem in our case (although it is rather inefficient), but for cases where we may be making a network call this can result in three separate network calls. Ideally we would want to be able to reuse an existing in-flight network call following device rotation.

The different instances of FusedLocationProvider mean that we’re having to incur this relatively large hit of creating the Play Services connection on a device rotation. I suspect that there may be some optimisations going on within Play Services which will reduce the load here, but it feels inefficient, nonetheless.

Common patterns we could use to share such instances across orientation changes would be to either use singleton objects which are not tied to the Activity of Fragment lifecycle, or we could use a headless, retained Fragment to maintain state between different Fragment instances. This is described here. But there is now a better way: use a ViewModel.

Personally I’m not a fan of the name ViewModel in this case because it really is not a direct analogue with a ViewModel that we’d use within an MVVM pattern, but it is more a mechanism of separating object state from Fragments and Activities. That said, it is a very useful thing. Let’s first look at how we implement one:

class LocationViewModel extends ViewModel {
    private LiveData<CommonLocation> locationLiveData = null;

    LiveData<CommonLocation> getLocation(Context context) {
        if (locationLiveData == null) {
            locationLiveData = new LocationLiveData(context);
        }
        return locationLiveData;
    }
}

We create a subclass of ViewModel which implements a single instance of our LiveData object. This will ensure that any consumers which request an instance of the LiveData object will receive the same one. This is a pattern which we could easily adopt without using ViewModel or even Architecture Components.

The interesting bit of ViewModel can be seen when we look at how we actually use this within our LocationFragment:

public class LocationFragment extends LifecycleFragment implements LocationListener {
    private static final String FRACTIONAL_FORMAT = "%.4f";
    private static final String ACCURACY_FORMAT = "%.1fm";

    private TextView latitudeValue;
    private TextView longitudeValue;
    private TextView accuracyValue;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        LocationViewModel locationViewModel = ViewModelProviders.of(getActivity()).get(LocationViewModel.class);
        LiveData<CommonLocation> liveData = locationViewModel.getLocation(context);
        liveData.observe(this, new Observer<CommonLocation>() {
            @Override
            public void onChanged(@Nullable CommonLocation commonLocation) {
                updateLocation(commonLocation);
            }
        });
    }

We obtain an instance of our LocationViewModel from ViewModelProviders, and from that we can obtain our LiveData instance. There is actually some interesting stuff going on behind the scenes here. ViewModelProviders.of() will return a ViewModelProvider instance which is essentially a retained Fragment within the Activity. That means that we can call this method from different instances of the same Activity (before and after a rotation), and we will receive the same instance of the ViewModelProvider. The get() method of ViewModelProviders will perform a cached lookup of the requested class, if a cached instance of that class already exists then the existing instance will be returned. We’ve already seen how our LocationViewModel will always return the same LocationLiveData instance.

So, in a nutshell, using this mechanism, if we request a LocationLiveData object both before and after a device rotation, then we’ll receive the same object instance.

If we run this code we get the following logs:

06-24 13:18:18.965 V/LocationViewModel: ⇢ ()
06-24 13:18:18.965 V/LocationViewModel: ⇠  [0ms]
06-24 13:18:18.966 V/LocationViewModel: ⇢ getLocation(context=com.stylingandroid.location.services.MainActivity@d48104c)
06-24 13:18:18.970 V/LocationLiveData: ⇢ (context=com.stylingandroid.location.services.MainActivity@d48104c)
06-24 13:18:18.976 V/LocationLiveData: ⇢ (this$0=com.stylingandroid.location.services.livedata.LocationLiveData@903038b)
06-24 13:18:18.976 V/LocationLiveData: ⇠  [0ms]
06-24 13:18:18.976 V/LocationLiveData: ⇠  [5ms]
06-24 13:18:18.976 V/LocationViewModel: ⇠ getLocation [9ms] = com.stylingandroid.location.services.livedata.LocationLiveData@903038b
06-24 13:18:19.035 V/LocationLiveData: ⇢ onActive()
06-24 13:18:19.036 V/LocationLiveData: ⇢ getFusedLocationProviderClient()
06-24 13:18:19.057 V/LocationLiveData: ⇠ getFusedLocationProviderClient [21ms] = com.google.android.gms.location.FusedLocationProviderClient@69fe57b
06-24 13:18:19.063 V/LocationLiveData: ⇠ onActive [28ms]
06-24 13:18:19.458 V/LocationLiveData: ⇢ onLocationResult(locationResult=LocationResult[locations: [Location[fused 51.753902,-0.461009 acc=20 et=+15d4h33m56s890ms]]])
06-24 13:18:19.459 V/LocationLiveData: ⇢ access$000(x0=com.stylingandroid.location.services.livedata.LocationLiveData@903038b, x1=com.stylingandroid.location.services.CommonLocation@398d437)
06-24 13:18:19.464 V/LocationLiveData: ⇠ access$000 [5ms]
06-24 13:18:19.464 V/LocationLiveData: ⇠ onLocationResult [6ms]
06-24 13:18:23.428 V/LocationLiveData: ⇢ onInactive()
06-24 13:18:23.428 V/LocationLiveData: ⇠ onInactive [0ms]
06-24 13:18:23.473 V/LocationViewModel: ⇢ getLocation(context=com.stylingandroid.location.services.MainActivity@128132f)
06-24 13:18:23.473 V/LocationViewModel: ⇠ getLocation [0ms] = com.stylingandroid.location.services.livedata.LocationLiveData@903038b
06-24 13:18:23.510 V/LocationLiveData: ⇢ onActive()
06-24 13:18:23.511 V/LocationLiveData: ⇢ getFusedLocationProviderClient()
06-24 13:18:23.511 V/LocationLiveData: ⇠ getFusedLocationProviderClient [0ms] = com.google.android.gms.location.FusedLocationProviderClient@69fe57b
06-24 13:18:23.511 V/LocationLiveData: ⇠ onActive [0ms]
06-24 13:18:23.518 V/LocationViewModel: ⇢ getLocation(context=com.stylingandroid.location.services.MainActivity@128132f)
06-24 13:18:23.518 V/LocationViewModel: ⇠ getLocation [0ms] = com.stylingandroid.location.services.livedata.LocationLiveData@903038b
06-24 13:18:23.521 V/LocationLiveData: ⇢ onInactive()
06-24 13:18:23.521 V/LocationLiveData: ⇠ onInactive [0ms]
06-24 13:18:23.529 V/LocationLiveData: ⇢ onActive()
06-24 13:18:23.530 V/LocationLiveData: ⇢ getFusedLocationProviderClient()
06-24 13:18:23.530 V/LocationLiveData: ⇠ getFusedLocationProviderClient [0ms] = com.google.android.gms.location.FusedLocationProviderClient@69fe57b
06-24 13:18:23.530 V/LocationLiveData: ⇠ onActive [1ms]
06-24 13:18:23.611 V/LocationLiveData: ⇢ onLocationResult(locationResult=LocationResult[locations: [Location[fused 51.753902,-0.461009 acc=20 et=+15d4h33m56s890ms]]])
06-24 13:18:23.612 V/LocationLiveData: ⇢ access$000(x0=com.stylingandroid.location.services.livedata.LocationLiveData@903038b, x1=com.stylingandroid.location.services.CommonLocation@a666946)
06-24 13:18:23.615 V/LocationLiveData: ⇠ access$000 [3ms]
06-24 13:18:23.615 V/LocationLiveData: ⇠ onLocationResult [3ms]

One obvious difference is that LocationViewModel is only initialised once, and if we look at the return values from each of the LocationViewModel#getLocation() method calls, the same object is actually returned (the reference address of the returned LocationLiveData objects is identical: 903038b), and this is also true of the FusedLocationProvider instances.

If we were actually triggering a service layer to make a network call from our LiveData object, then it would be relatively easy to have have a change in Fragment or Activity instances observe it while the network call is in flight. But once the network call returns then whatever is observing that LiveData object at the time will get the network response, even if it is different to the one which triggered the network call in the first place.

Another way that ViewModel can be used is to share and marshal data between Fragments. Normally we would do this via the parent Activity, but if both Fragments can obtain the same LiveData instance then one Fragment can observe a specific LiveData instance and it will be triggered if the other Fragment changes the value of that LiveData object.

We’re not going to cover the Room Persistence Library as part of this series because it is worthy of its own series – there is rather more to cover than this. Also, it does not work well with the LocationProvider example we have as this is real time data which really does not require storage. However, in the final article in this series we’ll take a look at what benefits the Architecture Components offer us in terms of robustness, code quality, and testability.

The source code for this article is available here.

Many thanks to Yiğit Boyar and Sebastiano Poggi for proof-reading services – this post would have been much worse without their input. Any errors or typos that remain are purely my fault!

© 2017, Mark Allison. All rights reserved.

Copyright © 2017 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

9 Comments

    1. It is not a Singleton. It is an instance stored in a retained fragment. It will not survive an Activity transition but will survive a fragment transition. It is intrinsically linked to the Activity lifecycle which a Singleton is not.

    1. It is the mechanism for obtaining the location and not the location which is being stored. So If we request a location update from one Fragment, then the user rotates the device, then the location update is still handled by the new Fragment. That is vastly different to storing the location in a singleton.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.