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 ap. This 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 second article we’ll take a look at LiveData.



Previously we saw how we could hook up different LocationProvider implementation to the Fragment lifecycle in different places depending on the actual implementation. However, a LocationProvider is actually an interesting case because it will continue to push location updates for as long as we’re subscribed to it (OK, the dummy implementation wouldn’t, but you get the idea). With our current implementation we have used a callback interface named LocationListener whose updateLocation() method will be called for each location update, but we can actually simplify this by using LiveData.

LiveData is an implementation of the publish / subscribe pattern for a data object. A LiveData object can be subscribed to by an interested party, and each time the data changes the subscriber will be notified. For those familiar with RxJava this is a very similar concept to an Observable object. The big advantage of LiveData is that a LiveData object will automatically hook onto an Activity or Fragment’s lifecycle and will only become active when the lifecycle is in STARTED , or RESUMED state. Symmetrically it will be shutdown when the lifecycle transitions to any state outside of that set.

Let’s see this in practice, first we’ll look at our dummy implementation:

LocationProvider.java public class LocationLiveData extends LiveData<CommonLocation> { private static final double LATITUDE = 51.649836; private static final double LONGITUDE = -0.401486; private static final float ACCURACY = 5f; private static final CommonLocation LOCATION = new CommonLocation(LATITUDE, LONGITUDE, ACCURACY); public LocationLiveData(Context context) { //NO-OP } @Override protected void onActive() { super.onActive(); setValue(LOCATION); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class LocationLiveData extends LiveData < CommonLocation > { private static final double LATITUDE = 51.649836 ; private static final double LONGITUDE = - 0.401486 ; private static final float ACCURACY = 5f ; private static final CommonLocation LOCATION = new CommonLocation ( LATITUDE , LONGITUDE , ACCURACY ) ; public LocationLiveData ( Context context ) { //NO-OP } @ Override protected void onActive ( ) { super . onActive ( ) ; setValue ( LOCATION ) ; } }

In this case we only want to emit an initial CommonLocation value once everything is initialised, and that is done in the onActive() method. We no longer need to hook into the Lifecycle – that is done automatically by LiveData. The onActive() and onInactive() methods will be called when we enter and exit active state ( STARTED , OR RESUMED ). So we no longer have the granularity of hooking into specific states, but if we needed that we can still implement lifecycle directly.

The important method here is when we call setValue(LOCATION) . This will both set the internal value of the LiveData object, but will also trigger updates to all active observers of this LiveData object.

Let’s now look at the changes that we need to make to our LocationFragment to consume updates from this LiveData implementation:

LocationFragment.java 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); LiveData<CommonLocation> liveData = new LocationLiveData(context); liveData.observe(this, new Observer<CommonLocation>() { @Override public void onChanged(@Nullable CommonLocation commonLocation) { updateLocation(commonLocation); } }); } . . . } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 ) ; LiveData < CommonLocation > liveData = new LocationLiveData ( context ) ; liveData . observe ( this , new Observer < CommonLocation > ( ) { @ Override public void onChanged ( @ Nullable CommonLocation commonLocation ) { updateLocation ( commonLocation ) ; } } ) ; } . . . }

We first create an instance of the LocationLiveData class that we just looked at. We then call its observe() method, which takes two arguments: the first is a LifecycleOwner which is what the LiveData object uses internally to determine that active/inactive state, and the second is an Observer<> implementation which will receive callbacks whenever the internal value of the LiveData object changes. In the case of a LocationProvider the onChanged() method will be called each time the location updates.

The rest of the LocationFragment is exactly the same as before.

Let’s now take a look at the Play Services LocationProvider implementation:

LocationProvider.java public class LocationLiveData extends LiveData<CommonLocation> { private final Context context; private FusedLocationProviderClient fusedLocationProviderClient = null; public LocationLiveData(Context context) { this.context = context; } @Override protected void onActive() { super.onActive(); if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return; } FusedLocationProviderClient locationProviderClient = getFusedLocationProviderClient(); LocationRequest locationRequest = LocationRequest.create(); Looper looper = Looper.myLooper(); locationProviderClient.requestLocationUpdates(locationRequest, locationCallback, looper); } @NonNull private FusedLocationProviderClient getFusedLocationProviderClient() { if (fusedLocationProviderClient == null) { fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context); } return fusedLocationProviderClient; } @Override protected void onInactive() { if (fusedLocationProviderClient != null) { fusedLocationProviderClient.removeLocationUpdates(locationCallback); } } private LocationCallback locationCallback = new LocationCallback() { @Override public void onLocationResult(LocationResult locationResult) { Location newLocation = locationResult.getLastLocation(); double latitude = newLocation.getLatitude(); double longitude = newLocation.getLongitude(); float accuracy = newLocation.getAccuracy(); CommonLocation location = new CommonLocation(latitude, longitude, accuracy); setValue(location); } }; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class LocationLiveData extends LiveData < CommonLocation > { private final Context context ; private FusedLocationProviderClient fusedLocationProviderClient = null ; public LocationLiveData ( Context context ) { this . context = context ; } @ Override protected void onActive ( ) { super . onActive ( ) ; if ( ActivityCompat . checkSelfPermission ( context , Manifest . permission . ACCESS_FINE_LOCATION ) ! = PackageManager . PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return; } FusedLocationProviderClient locationProviderClient = getFusedLocationProviderClient ( ) ; LocationRequest locationRequest = LocationRequest . create ( ) ; Looper looper = Looper . myLooper ( ) ; locationProviderClient . requestLocationUpdates ( locationRequest , locationCallback , looper ) ; } @ NonNull private FusedLocationProviderClient getFusedLocationProviderClient ( ) { if ( fusedLocationProviderClient == null ) { fusedLocationProviderClient = LocationServices . getFusedLocationProviderClient ( context ) ; } return fusedLocationProviderClient ; } @ Override protected void onInactive ( ) { if ( fusedLocationProviderClient ! = null ) { fusedLocationProviderClient . removeLocationUpdates ( locationCallback ) ; } } private LocationCallback locationCallback = new LocationCallback ( ) { @ Override public void onLocationResult ( LocationResult locationResult ) { Location newLocation = locationResult . getLastLocation ( ) ; double latitude = newLocation . getLatitude ( ) ; double longitude = newLocation . getLongitude ( ) ; float accuracy = newLocation . getAccuracy ( ) ; CommonLocation location = new CommonLocation ( latitude , longitude , accuracy ) ; setValue ( location ) ; } } ; }

There’s a little bit more going on here, but it’s fairly easy to understand. When the LifecycleOwner enters an active state, then onActive() is called. We first check that we have the necessary permissions and only attempt to register for location updates if we do. If we go though the permission request user flow, then the LocationFragment will become inactive while permissions are requested from the user. When the user returns to LocationFragment then onActive() will be called again, so we will correctly register for location updates if permission was granted.

Whenever the LifecycleOwner exits an active state, then onInactive() is called and we’ll unregister for location updates, if we have a valid FusedLocationProvider instance.

An important point here is that a LiveData object may have more than one observer. If that is the case then it will enter an active state when any of the observers becomes active; but will only become inactive when all of the observers are inactive.

The remainder of the code in here is just to handle location updates, and in onLocationResult() we call setValue() which will update the internal value and make a callback to any active observers.

Using LiveData is a really simple approach when it comes to managing data which will change periodically, and it removes the need to manually hook onto lifecycle changes.

Hopefully it is now obvious why an explanation of the lifecycle components was necessary to make the understanding of LiveData somewhat easier.

In the next article in this series we’ll take a look at ViewModel and see how that can work in conjunction with LiveData to further make things easier for us.

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.

Related

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.