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 lifecycle components.



In a recent article on LocationServices we had a Fragment implementation which was tightly coupled to the Play Services location provider. This is problematic because, if we need to use different location providers for different build variants then this tight coupling to our Fragment makes it difficult to abstract away. Architecture Components provides us with a new lifecycle framework which gives us the ability to invert the dependencies, so that our Fragment implementation is completely agnostic of the location provider.

To demonstrate how we can do this, we’ll create a project with two different location providers for different product flavors. One will use the Play Services location provider which we covered in the previous article, and the second will use a dummy provider which will always return the location of Vicarage Road Stadium (home of Watford Football Club).

Let’s first take a look at the build.gradle for our app module:

app/build.gradle apply plugin: 'com.android.application' ext { supportLibraryVersion = "25.4.0" playServicesVersion = "11.0.1" architectureComponentsVersion = "1.0.0-alpha3" } android { compileSdkVersion 25 buildToolsVersion "25.0.2" defaultConfig { applicationId "com.stylingandroid.location.services" minSdkVersion 17 targetSdkVersion 25 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } lintOptions { disable 'OldTargetApi', "GradleDependency" } productFlavors { dummy playServices } } dependencies { compile "com.android.support:appcompat-v7:${supportLibraryVersion}" compile "com.android.support:design:${supportLibraryVersion}" compile "com.android.support:support-v4:${supportLibraryVersion}" compile 'com.android.support.constraint:constraint-layout:1.1.0-beta1' compile "android.arch.lifecycle:runtime:${architectureComponentsVersion}" compile "android.arch.lifecycle:extensions:${architectureComponentsVersion}" annotationProcessor "android.arch.lifecycle:compiler:${architectureComponentsVersion}" playServicesCompile "com.google.android.gms:play-services-location:{playServicesVersion}" } 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 apply plugin : 'com.android.application' ext { supportLibraryVersion = "25.4.0" playServicesVersion = "11.0.1" architectureComponentsVersion = "1.0.0-alpha3" } android { compileSdkVersion 25 buildToolsVersion "25.0.2" defaultConfig { applicationId "com.stylingandroid.location.services" minSdkVersion 17 targetSdkVersion 25 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile ( 'proguard-android.txt' ) , 'proguard-rules.pro' } } lintOptions { disable 'OldTargetApi' , "GradleDependency" } productFlavors { dummy playServices } } dependencies { compile "com.android.support:appcompat-v7:${supportLibraryVersion}" compile "com.android.support:design:${supportLibraryVersion}" compile "com.android.support:support-v4:${supportLibraryVersion}" compile 'com.android.support.constraint:constraint-layout:1.1.0-beta1' compile "android.arch.lifecycle:runtime:${architectureComponentsVersion}" compile "android.arch.lifecycle:extensions:${architectureComponentsVersion}" annotationProcessor "android.arch.lifecycle:compiler:${architectureComponentsVersion}" playServicesCompile "com.google.android.gms:play-services-location:{playServicesVersion}" }

We define the two flavors named dummy and playServices ; include the necessary lifecycle libraries and annotation processor; and include play-services-location but only for the playServices flavor.

We can then set up separate source trees for the two build flavors, each will contain its own LocationProvider implementation:

This is easy enough we can quite easily create this set up without any help from Architecture Components. Different location providers may have differing requirements as to where we need to hook them into the Fragment lifecycle. For the Play Services provider we want to kick off the registration early because it will be around 300ms until we get our first location update. However the dummy provider will be almost instantaneous. So we need to hook the Play Services implementation into onStart , whereas the dummy implementation will generate an immediate update, so we want to wait until later in the lifecycle (specifically onResume ) to trigger it. Although this is a somewhat contrived example, different location providers may require different things to happen at different stages of the Fragment lifecycle.

One way of solving this would be to have the Fragment implementation make calls in to the LocationProvider at every lifecycle state transition. Then the LocationProvider is responsible for performing the necessary actions when certain lifecycle state transitions occur, but this requires us to add a bit of boilerplate to our Fragment implementation. If we then have other abstractions to external services, we need to start duplicating this for each of them.

This is where the Architecture Lifecycle Components can help us. They allow us to hook an external component, such as our LocationProvider, to the Fragment lifecycle with a minimum of code.

Let’s take a look at our LocationFragment 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); new LocationProvider(getContext(), getLifecycle(), this); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_location, container, false); latitudeValue = (TextView) view.findViewById(R.id.latitude_value); longitudeValue = (TextView) view.findViewById(R.id.longitude_value); accuracyValue = (TextView) view.findViewById(R.id.accuracy_value); return view; } @Override public void updateLocation(CommonLocation location) { String latitudeString = createFractionString(location.getLatitude()); String longitudeString = createFractionString(location.getLongitude()); String accuracyString = createAccuracyString(location.getAccuracy()); latitudeValue.setText(latitudeString); longitudeValue.setText(longitudeString); accuracyValue.setText(accuracyString); } private String createFractionString(double fraction) { return String.format(Locale.getDefault(), FRACTIONAL_FORMAT, fraction); } private String createAccuracyString(float accuracy) { return String.format(Locale.getDefault(), ACCURACY_FORMAT, accuracy); } } 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 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 ) ; new LocationProvider ( getContext ( ) , getLifecycle ( ) , this ) ; } @ Override public void onCreate ( @ Nullable Bundle savedInstanceState ) { super . onCreate ( savedInstanceState ) ; } @ Override public View onCreateView ( LayoutInflater inflater , ViewGroup container , Bundle savedInstanceState ) { View view = inflater . inflate ( R . layout . fragment_location , container , false ) ; latitudeValue = ( TextView ) view . findViewById ( R . id . latitude_value ) ; longitudeValue = ( TextView ) view . findViewById ( R . id . longitude_value ) ; accuracyValue = ( TextView ) view . findViewById ( R . id . accuracy_value ) ; return view ; } @ Override public void updateLocation ( CommonLocation location ) { String latitudeString = createFractionString ( location . getLatitude ( ) ) ; String longitudeString = createFractionString ( location . getLongitude ( ) ) ; String accuracyString = createAccuracyString ( location . getAccuracy ( ) ) ; latitudeValue . setText ( latitudeString ) ; longitudeValue . setText ( longitudeString ) ; accuracyValue . setText ( accuracyString ) ; } private String createFractionString ( double fraction ) { return String . format ( Locale . getDefault ( ) , FRACTIONAL_FORMAT , fraction ) ; } private String createAccuracyString ( float accuracy ) { return String . format ( Locale . getDefault ( ) , ACCURACY_FORMAT , accuracy ) ; } }

The first thing we must do is extend LifecycleFragment. This is a temporary measure while the Architecture Components library is not yet at a stable release. Once it has been released then the support library Fragment implementation will contain the necessary code and we need not bother with this.

We also implement LocationListener which is a callback interface which we’ll look at in a moment.

In onAttach() we create our LocationProvider instance which takes a Context, a Lifecycle, and an instance of LocationListener (which LocationFragment implements). The interesting one here is the Lifecycle – this is obtained from the getLifecycle() method, and it is this that is currently implemented in LifecycleFragment, but will move to Fragment once the library is fully released. The Lifecycle is an abstraction of the Fragment lifecycle and is what our LocationProvider uses to get notified of Fragment lifecycle state transitions.

Finally we implement the one method which is defined in LocationListener: updateLocation() . This will get called whenever our LocationProvider gets updated location data.

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

LocationProvider.java public class LocationProvider implements LifecycleObserver { 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); private final Lifecycle lifecycle; private final LocationListener locationListener; public LocationProvider(@NonNull Context context, Lifecycle lifecycle, @NonNull LocationListener listener) { this.lifecycle = lifecycle; this.locationListener = listener; lifecycle.addObserver(this); } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) void unregisterObserver() { lifecycle.removeObserver(this); } @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) void sendDummyLocation() { locationListener.updateLocation(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 public class LocationProvider implements LifecycleObserver { 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 ) ; private final Lifecycle lifecycle ; private final LocationListener locationListener ; public LocationProvider ( @ NonNull Context context , Lifecycle lifecycle , @ NonNull LocationListener listener ) { this . lifecycle = lifecycle ; this . locationListener = listener ; lifecycle . addObserver ( this ) ; } @ OnLifecycleEvent ( Lifecycle . Event . ON_DESTROY ) void unregisterObserver ( ) { lifecycle . removeObserver ( this ) ; } @ OnLifecycleEvent ( Lifecycle . Event . ON_RESUME ) void sendDummyLocation ( ) { locationListener . updateLocation ( LOCATION ) ; } }

First we must implement LifecycleObserver to receive lifecycle callback. Next we need to register the class as an observer of the lifecycle. Finally we specify which lifecycle events we’re interested in using the @OnLifecycleEvent annotations, and the annotated methods will get called whenever those lifecycle events occur.

The PlayServices LocationProvider uses the same approach, but actually hooks up to different parts of the Fragment lifecycle:

LocationProvider.java public class LocationProvider implements LifecycleObserver { private final Context context; private final Lifecycle lifecycle; private final LocationListener locationListener; private FusedLocationProviderClient fusedLocationProviderClient = null; public LocationProvider(@NonNull Context context, Lifecycle lifecycle, @NonNull LocationListener listener) { this.context = context; this.lifecycle = lifecycle; this.locationListener = listener; lifecycle.addObserver(this); } @OnLifecycleEvent(Lifecycle.Event.ON_START) void registerForLocationUpdates() { 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; } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) void unregisterForLocationUpdates() { if (fusedLocationProviderClient != null) { fusedLocationProviderClient.removeLocationUpdates(locationCallback); } } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) void unregisterObserver() { lifecycle.removeObserver(this); } private LocationCallback locationCallback = new LocationCallback() { @Override public void onLocationResult(LocationResult locationResult) { super.onLocationResult(locationResult); Location lastLocation = locationResult.getLastLocation(); double latitude = lastLocation.getLatitude(); double longitude = lastLocation.getLongitude(); float accuracy = lastLocation.getAccuracy(); CommonLocation location = new CommonLocation(latitude, longitude, accuracy); locationListener.updateLocation(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 50 51 52 53 54 55 56 57 58 59 60 public class LocationProvider implements LifecycleObserver { private final Context context ; private final Lifecycle lifecycle ; private final LocationListener locationListener ; private FusedLocationProviderClient fusedLocationProviderClient = null ; public LocationProvider ( @ NonNull Context context , Lifecycle lifecycle , @ NonNull LocationListener listener ) { this . context = context ; this . lifecycle = lifecycle ; this . locationListener = listener ; lifecycle . addObserver ( this ) ; } @ OnLifecycleEvent ( Lifecycle . Event . ON_START ) void registerForLocationUpdates ( ) { 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 ; } @ OnLifecycleEvent ( Lifecycle . Event . ON_STOP ) void unregisterForLocationUpdates ( ) { if ( fusedLocationProviderClient ! = null ) { fusedLocationProviderClient . removeLocationUpdates ( locationCallback ) ; } } @ OnLifecycleEvent ( Lifecycle . Event . ON_DESTROY ) void unregisterObserver ( ) { lifecycle . removeObserver ( this ) ; } private LocationCallback locationCallback = new LocationCallback ( ) { @ Override public void onLocationResult ( LocationResult locationResult ) { super . onLocationResult ( locationResult ) ; Location lastLocation = locationResult . getLastLocation ( ) ; double latitude = lastLocation . getLatitude ( ) ; double longitude = lastLocation . getLongitude ( ) ; float accuracy = lastLocation . getAccuracy ( ) ; CommonLocation location = new CommonLocation ( latitude , longitude , accuracy ) ; locationListener . updateLocation ( location ) ; } } ; }

So we have easily been able to hook up these different implementations to different parts of the Fragment lifecycle without having to leak any implementation specific in to the LocationFragment code.

If we consider what is going on internally, it’s pretty much the approach we looked at earlier: the Lifecycle object will make callbacks to any registered observers for every lifecycle state transition, and it is down to the individual observers to decide which ones they are interested in. However the additional code that we’ve had to add to the LocationFragment is actually pretty minimal, and this will reduce even further once the Architecture Components library is fully released and integrated with the Android framework.

While this certainly benefits us, there is actually a better way of doing it. I have quite deliberately done an initial implementation by hooking on to lifecycle events for two reasons: firstly, it is an important foundation for some of the principles which we’ll be covering later in this series; and, secondly, the lifecycle approach certainly is useful in some circumstances and is useful to know! In the next article we’ll take things a stage further, and see how we can further simplify our LocationFragment.

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.