React Native Google Location for Android (Image by Nina Hagn)

React Native Android Google Play Services Geolocation Module

(or how to go native and still know where you are…)

For a cross-Platform Project we needed a Geolocation Service – and as at the time off initialization
React Native 14 didn't have their own Geolocation Polyfill ready for Android yet – I thought,
let's just “howl with the wolves” and use Google Play Services - Geolocation.
So let's dive in in right away and start developing a RN-Module for just this occasion.

If you're only starting with Google Play Services, read up on it in the awesome Blog Post by Treehouse here.
By the way, I'm going to use their LocationProvider.java for conveniences sake in this Post,
so have a look at their github-Repo, too.

Wanna have a peek at the final code / module? Look at 'react-native-google-location' on npmjs.com
or read through the code of my bitbucket-git Repo.

Starting up and getting Ready to Roll

But first we have to start a new Project, and why do it the hard way, when react-native-cli will do the basics for us in a swift call to:

react-native init RNGLocation

Now we only have to clean up our project directory and change some lines of code to be ready to fly
– else we'd have to write every Project and Gradle File by hand ; ).

The easiest part:
Delete ios and node_modules directories as we won't need them in an Android oriented Library.

Next to package.json:
If you want to push your final Module to npm or import it locally, run a quick

npm init 

and answer the questions asked. Naming should (for ease of finding your module on npmjs and others)
follow “react-native-YOURMODULENAME”. Multiple keywords can be added comma-separated.

Open up package.json and change "dependencies" to "peerDependencies", as we won't need own nodejs – modules in our library.

On cue of 'library' we now start hacking – so import our Project in Android Studio (or your Weapon of Choice - IntelliJ would be a preference,
but I've also just used Android Studio for the Java side and PhpStorm for the JavaScript part) with File > New > Import Project…
and select the “android” Directory. Studio will import from the existing Gradle Files.

After loading you might have an empty screen at first, so click on “1: Project” on the left hand Pane
and open up build.gradle (Module: app) from Gradle Scripts. Change:

apply plugin: “com.android.application”

to

apply plugin “com.android.library”

and add location services to dependencies:

dependencies {
    …
    compile 'com.google.android.gms:play-services-location:8:3.0'
}

Open settings.gradle and remove the line rootProject.name... - it's a Library after all.

Open up the AndroidManifest.xml and remove the whole <application>-node. Add:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Now be done (that is to say delete) with MainActivity from your app/java Folder and we're ready to roll.

Rolling to Location

As I wrote above, we're going to use LocationProvider.java from Treehouse's github-Repo,
so if you haven't done so already, clone the git Repo and copy the file from app/src/main/java/teamtreehouse/com/iamhere/
into your projects java folder with Android Studio.
The package will be changed to yours automatically.

Next create a new Java Class named RNGLocationModule.

As written in Facebook's Native Module Tutorial we have to let our class extend ReactContextBaseJavaModule.
As we want to provide a LocationCallback to JavaScript, we have to implement LocationProvider.LocationCalback.

Studio will now throw some errors in your face (as the class is empty right now),
we have to import ReactContextBaseJavaModule and implement the required functions and constructor.

But no fear, with a press of Alt+Enter on ReactContextBaseJavaModule, another on the public class ... line
and choosing “Implement Methods” and another with choosing “Create constructor matching super”,
Studio will do it for you. Remember Alt+Enter, as we will have to use it, whenever Studio shouts for another package to be imported.

As 'null' is not a nice name, add a class constant with

// React Class Name as called from JS
public static final String REACT_CLASS = "RNGLocation";

and return it instead of 'null' in getName.

Next we'll have to set some variables for our Location Provider and React Native Context – and a TAG to Log back to Android Console:

// Unique Name for Log TAG
public static final String TAG = RNGLocationModule.class.getSimpleName();
// Save last Location Provided
private Location mLastLocation;
// The Google Play Services Location Provider
private LocationProvider mLocationProvider;
//The React Native Context
ReactApplicationContext mReactContext;

But our Library is still doing nothing, so let's set up the Location Provider in the constructor:

// Constructor Method as called in Package
public RNGLocationModule(ReactApplicationContext reactContext) {
    super(reactContext);
    // Save Context for later use
    mReactContext = reactContext;

    // Get Location Provider from Google Play Services
    mLocationProvider = new LocationProvider(mReactContext.getApplicationContext(), this);

    // Check if all went well and the Google Play Service are available...
    if (!mLocationProvider.checkPlayServices()) {
        Log.i(TAG, "Location Provider not available...");
    } else {
        // Connect to Play Services
        mLocationProvider.connect();
        Log.i(TAG, "Location Provider successfully created.");
    }
}

Yeah! At last we have a Location Provider – now let's do something with it and send it back to JavaScript.
First implement a getLocation function to be called by JS as follows:

/*
 * Location Provider as called by JS
 */
@ReactMethod
public void getLocation() {
    if (mLastLocation != null) {
        try {
            double Longitude;
            double Latitude;

            // Receive Longitude / Latitude from (updated) Last Location
            Longitude = mLastLocation.getLongitude();
            Latitude = mLastLocation.getLatitude();

            Log.i(TAG, "Got new location. Lng: " + Longitude+" Lat: " + Latitude);

            // Create Map with Parameters to send to JS
            WritableMap params = Arguments.createMap();
            params.putDouble("Longitude", Longitude);
            params.putDouble("Latitude", Latitude);

            // Send Event to JS to update Location
            sendEvent(mReactContext, "updateLocation", params);
        } catch (Exception e) {
            e.printStackTrace();
            Log.i(TAG, "Location services disconnected.");
        }
    }
}

But wait – what is sendEvent? :

/*
 * Internal function for communicating with JS
 */
private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
    if (reactContext.hasActiveCatalystInstance()) {
        reactContext
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit(eventName, params);
    } else {
        Log.i(TAG, "Waiting for CatalystInstance...");
    }
}

No great magic here. First we check, if your React Native App has already started (else whereto should we send and Event?)
then we emit it by RN-Brigdges DeviceEventManagerModule class.

You could write your Package class now and be done with it (as getLocation emits to a JavaScript Callback).
But we want our Location Updates immediately as they arrive, so let's throw in some code to handleNewLocation:

/*
 * Location Callback as defined by LocationProvider
 */
@Override
public void handleNewLocation(Location location) {
    if (location != null) {
        mLastLocation = location;
        Log.i(TAG, "New Location..." + location.toString());
        getLocation();
    }
}

Now getLocation is called automatically – but only if it's a valid Location.
Last but not least, we won't have our Location Provider running, after our App is done.
So inject a destructor:

@Override
protected void finalize() throws Throwable {
    super.finalize();
    // If Location Provider is connected, disconnect.
    if (mLocationProvider != null && mLocationProvider.connected) {
        mLocationProvider.disconnect();
    }
}

Now let's package it up as written in Facebook's Native Module Tutorial (only clean and complete ; ).
Create a new Java Class named RNGLocation and insert following code:

package com.rnglocation;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class RNGLocation implements ReactPackage {
    /**
     * This function returns the Module List (we have only one, 
     * but RN wants a List anyway) of our RNGLocationModule.
     * @param reactContext
     * @return ModuleList
     */
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        return Arrays.<NativeModule>asList(
                new RNGLocationModule(reactContext)
        );
    }

    /**
     * These functions return empty Lists as we have no View or JS Module 
     */
    @Override
    public List<Class<? extends JavaScriptModule>> createJSModules() {
        return Collections.emptyList();
    }
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactApplicationContext) {
        return Collections.emptyList();
    }
}

Now we can breathe easily – the Java Part is (nearly, sorry to say) done.
Open up your index.android.js to provide and export our Native Module:

/**
 * @providesModule RNGLocation
 */

'use strict';

/**
 * This Module returns Location from Google Play Services Location for Android
 */

var { NativeModules } = require('react-native');
module.exports = NativeModules.RNGLocation;

That's all folks... – not. Let's play nice and add an Error Message for iOS Developers.
So open index.ios.js and hack:

'use strict';

let React = require('react-native');

let {
  Component,
} = React;

let ERROR = 'RNGLocations is not available in iOS - use Geolocation Polyfill.';

class RNGLocations extends Component {
  constructor (props) {
    super(props);
    console.log(ERROR);
  }

  getLocation () { console.error(ERROR) }
}

module.exports = RNGLocations;

If you don't want to publish it to npm just add

"react-native-YOURMODULENAME": "file://PATH/TO/YOUR/MODULE/RNGLocation"

to your apps package.json.

Now we only have to register it in your App's build.gradle, settings.gradle and MainActivity.java as provided
in my 'react-native-google-location' bitbucket-git Repo.
Follow the steps after “Add it to your android project” and substitute 'react-native-google-location' with 'react-native-YOURMODULENAME' ; ).

In my bitbucket-git Repo you can also find a small Example
how to implement the finished Module in an React Native App.

So now you know how to go native with React Native Android.
That's (really) all, folks!

0 Kommentare