Loading Google Maps in Cordova the Right Way

There are a lot of articles out there that talk about using Google Maps in a Cordova or Telerik AppBuilder project. A surprising number of them start with:

<script src="https://maps.googleapis.com/maps/api/js?key=API_KEY&sensor=true"></script>

NOOOOO

If this doesn’t strike you as being extremely wrong, think about it this way; devices aren’t always online. What happens when someone opens your app while offline, then wanders into cell or wifi range and is online again? Your maps will fail to work even though they are now online, since the maps API couldn’t be loaded when the app was opened!

The right thing to do is to load the Google Maps API when you are online, not using a <script> tag.

You can see a working example here: https://github.com/rally25rs/cordova-google-maps-sample/

Here is my solution to this issue, using jQuery.

First, we set up a “deviceready” event listener, as normal.

(function (global) {
    "use strict";

    function onDeviceReady () {
    }

    document.addEventListener("deviceready", onDeviceReady, false);
})(window);

Once we receive the deviceready event, we can also add a handler for the online event. This is a Cordova event that will be fired when the device state moves from offline to online. We also want a listener for the resume event. This event is fired when the app is brought back into focus after being backgrounded.

The goal here will be to handle any case where the device was offline when your app is loaded. If the device comes online while the app is opened, we will get the “online” event. If the app is opened then backgrounded, then the device comes online and the app is brought back up, we will get the “resume” event.

On all 3 of these events; deviceready, online, and resume we want to load our Google Maps API javascript if it hasn’t already been loaded.

(function (global) {
    "use strict";

    function onDeviceReady () {
        document.addEventListener("online", onOnline, false);
        document.addEventListener("resume", onResume, false);
        loadMapsApi();
    }

    function onOnline () {
        loadMapsApi();
    }

    function onResume () {
        loadMapsApi();
    }

    function loadMapsApi () {
        // if online and maps not already loaded
        //    then load maps api
    }

    document.addEventListener("deviceready", onDeviceReady, false);
})(window);

Now in the loadMapsApi() function, we can just return if we are offline, or if maps is already loaded. We can determine if we are online using the Cordova API which exposes the network status through navigator.connection, and we can check google.maps to see if maps is already loaded.

    function loadMapsApi () {
        if (navigator.connection.type === Connection.NONE || (global.google !== undefined && global.google.maps)) {
            return;
        }
        // load maps api
    }

Next we need to go and fetch the actual Google Maps API JavaScript. If you are using jQuery, you can use the jQuery.getScript(url) function to do this. We also add a query string parameter "callback=onMapsApiLoaded". Remember that loading the JavaScript for the maps API is asynchronous. Google provides this query string parameter to give us a way to provide a callback that will be executed when the maps API has finished loading and is ready to be used.

(function (global) {
    "use strict";

    function onDeviceReady () {
        document.addEventListener("online", onOnline, false);
        document.addEventListener("resume", onResume, false);
        loadMapsApi();
    }

    function onOnline () {
        loadMapsApi();
    }

    function onResume () {
        loadMapsApi();
    }

    function loadMapsApi () {
        if(navigator.connection.type === Connection.NONE || google.maps) {
            return;
        }
        $.getScript('https://maps.googleapis.com/maps/api/js?key=API_KEY&sensor=true&callback=onMapsApiLoaded');
    }

    global.onMapsApiLoaded = function () {
        // Maps API loaded and ready to be used.
        var map = new google.maps.Map(document.getElementById("map"), {});
    };

    document.addEventListener("deviceready", onDeviceReady, false);
})(window);

There you have it! This is a much better way of loading the Maps API, and really any other external scripts. As a general rule, any time you are doing a hybrid Cordova app, you should never have a <link> or <script> tag that references an external URL. Only use these tags to load css and js that are deployed with your app, or that you are loading off the device and won’t need a network connection.

Advertisements
Tagged with: , , ,
Posted in JavaScript, Mobile, Programming
33 comments on “Loading Google Maps in Cordova the Right Way
  1. leandro says:

    Hi, excellent articule, I want implement but the map isn’t load, and when change the device (in ripple) I get the error: ” Cannot set property ‘onMapsApiLoaded’ of undefined”, I define the “global” un the (function(global)… but it is undefined. thanks

    • rally25rs says:

      That would mean that `window` is undefined in Ripple, or your code is not correctly passing `window` to the function. You could try changing the code from `global.onMapsApiLoaded` to `window.onMapsApiLoaded`. If that also does not work, then there is no `window` global object in Ripple, which would be weird. If that does work, then your code is not set up to correctly pass `window` into the immediately-invoked-function.

  2. leandro says:

    hi, thanks for your answer. I can solve some problems, the global now works, I forgot the “window” inside () below the general function. Now enter inside of onMapsApiLoaded() an execute the google.maps.Map(), but I can’t see it in the ripper emulator, in android does not work either, the VS show me “DOM explorer”, in the div with id=”map” I can see others div, I think the map is inside, but not render in the emulator. thanks

  3. leandro says:

    hi, do you have a example that you can publish for download?, where the map works or if you want I can publish the example that I am developing

  4. truespursfan says:

    Great article. Is there a way to make it aware of a user’s current location? I’ve currently got the above code working fine but I’m now trying to find the user’s location using the “navigator.geolocation.watchPosition” code from the Cordova Geolocation plugin (http://plugins.cordova.io/#/package/org.apache.cordova.geolocation) but currently getting nowhere 😦

    Thanks again.

  5. Brad says:

    When the map displays it only shows the top left corner. Does a google map resize call need to be made somewhere to refresh the map layout to the full wide/height?

    • rally25rs says:

      Yes, you should set your element that contains the map to be some size in CSS. I think in my sample that I used I just had something like: <div id=”map”> and in CSS: #map { width: 100%; height: 500px; } but of course you should size it to be whatever fits your app’s layout.

  6. Philip McGrath says:

    I keep getting a Connection is undefined when I use Connection.NONE, any ideas why ?

  7. Francisco says:

    hi. I don’t understand what is (window) at end of code. i never see something like that.

  8. Terri Morgan says:

    Have been trying to get Google Maps to work on a ‘second page’ in a Cordova app (emulator for Android or iPhone both function the same). Suggestions or fixes would be super!

    I have an index.html page that runs your index.js script. Works great. The Ripple emulator loads, the map script loads and, bonus, I have added a “where am I” script that runs on a link and sets a map pointer to where I am. Wonderful!

    But, it only works once.

    a) I have set a link to a second page (within the app). For the sake of my sanity, I replicated the code from the index.html and index.js files and set up local.html and local.js.

    I’d like to be able to navigate to the new page and have the map load (same as on the initial start up). It does not.

    b) Same issue if I navigate back to the the index.html page — no map. It only works when 1) I start the emulator or 2) I use parent.location.reload in the link (to reload the whole window).

    Seems to be related to firing deviceready.

    I’ve tried multiple options (day 3 now), including variations of window.onload and a separate function that should force deviceready to fire, but none of what I have tried worked.

    I’m a bit new to mobile development and am not a full-time coder — what am I missing?

    Options? Suggestions?

  9. Hi! Very nice “best practices” post thanks! The question I have though is how did you setup your API key for the JS API? I’ve heard most have setup using the “Browser” key type with a [blank] or “*” in the Allowed Referrers field. But I’m looking to secure it for my PhoneGap app only. Thoughts/Ideas/Suggestions? Thanks in advance!

    • rally25rs says:

      I’ve generally just used the API key by setting the referrers to blank (“”). I believe that Cordova sets the host to “” so when the HTTP request is sent to Google, the referrer will always be blank. I haven’t tried this myself, but if you want to further secure it to only your application, you may want to look at Google Maps for Work (the paid version of Google Maps) which does not use an API key, but I’m not sure if it is still tied to a referrer or not. The Google maps for Android (native) applications also seems to use an API key that is tied to your application’s certificate, so I think that would probably restrict your API key to only be used from your specific app, but I doubt that API key would directly work with the JavaScript Maps API. So I guess I don’t really have a good answer… sorry I’m not of more help!

      • I have looked at PhoneGap/Cordova plugins that use the native (Android/iOS) keys that would be easier to restrict, but I have seen so many PhoneGap/Cordova posts using the JavaScript API and use the blank referrer and state that you can secure it but they don’t show it for the simple tutorial sake. Your post just happened to be the most recent so thought I’d check. Thanks for the response and confirming what I’ve been finding!

  10. Your code may end up with lots of script tags inside your page. Take a look at my answer at http://stackoverflow.com/a/30631656/873650. Also, using getScript you cannot take advantage of browser caching, but you would use ajax with cache:true option instead. I’ve notice that cache can load google maps API when your app is started offline.

  11. Marcel says:

    Thanks for this article. It’s amazing how few places mention this critical issue.

    I’m running into trouble where the maps API script is getting added more than once, which causes trouble (at the very least the gmaps script emits a warning about getting loaded more than once). This happens if the internet is “available” from navigator.connection’s perspective, but it so slow/lossy or the connection goes to zero after that guard statement was evaluated. If the user foregrounds/backgrounds the app, then we add the google maps script more than once. When internet is restored then the script is evaluated more than once and I get the warning message (and things break).

    I can’t find a way to make sure I’m only added the script once, and no more. If I use jQuery’s Ajax method to load google maps, then it won’t call my error callback handler because it’s a cross-domain ajax call. (jQuery makes this behavior clear in the documentation.)

    If I set a timeout on the Ajax call, the error handler gets called, but if internet is restored the script will evaluate, in spite of it being “timed out”. I tried called jqXHR.abort() in the timeout handler, but that had no effect.

    Any ideas? Thanks

  12. Leonardo says:

    I will not have problems if I upload the application to the store? apparently it checks whether you are using a javascript API android application: S

  13. Jepz says:

    wow, many thanks. I used to show some map using google map api and it made me confuse but with your idea helping me a lot.

  14. Where would this piece of code go in Ionic? I don’t understand. does it go in the index.html or in a controller js file?

    • rally25rs says:

      It would go wherever the onDeviceReady event handler is in your code, which is a standard Cordova/PhoneGap callback function. I’ve never done an Ionic app so I’m not sure how they are usually structured, but I assume there has to be some main JS file that is run on startup. That would likely be the place to put this code.

  15. Luciano Lima says:

    There is a way to limit the use of API_KEY only in apps?
    In Developers Console, it’s a browser key with empty referrers. Then anyone can use the API_KEY in any website and app.

  16. dimitri3ds says:

    Very nice article. One problem is that if the network connection is broken just after the callback function (e.g. onMapsApiLoaded) is called, the google api still misses some dependencies which are normally loaded after the callback function (e.g. marker.js util.js map.js etc). This means that the created map is not functional even though google.maps is defined. These resources aren’t loaded automatically when the network is back online.
    Do you know of a way to check that the api is fully loaded (with all the dependencies – not just the callback function)? And also how could we make the api reload.

    • Hi Dimitri3ds,

      I’m in the same situation. Have you found a solution for this problem?

    • mpoisot says:

      dimitri3ds, I thought the whole point of the callback parameter passed to the googlemaps url was that it would only get called after the library (and all sub-libraries) are ready to be used. Otherwise what’s the point of it?

      Did you try to file this as a bug? If so, please paste the tracker url here.

      That’s a pretty tough scenario to replicate. Are you using a tool for that? I’m imagining a developer-oriented browser plugin that allows you to disable the network from a JS call. So you could put a call to `Network.disable()` in the first line of the onMapsApiLoaded callback function to try to make it happen.

  17. Ricardo says:

    Change the code from:

    global.google !== undefined && global.google.maps

    To:

    typeof google === ‘object’ && typeof google.maps === ‘object’

    Reason:

    The variable google is undefined and it will throw an exception because the google maps script is still not loaded in this stage.

  18. Erwin says:

    So true, but apparantly I needed to read it like this to realise I was doing it in the wrong way!

  19. AtulV says:

    Useful article. However, it is weird that you forget to mention that in order to have the navigator.connection object you need to include the cordova network information plugin.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

CodingWithSpike is Jeff Valore. A professional software engineer, focused on JavaScript, Web Development, C# and the Microsoft stack. Jeff is currently a Software Engineer at Virtual Hold Technologies.


I am also a Pluralsight author. Check out my courses!

%d bloggers like this: