Google Maps for Android Pt 2: User Location

In the previous article of this series, I covered how to set up Google Maps for Android, allowing you to display a map in your application that focused on a specific location. While that tutorial covered the important steps behind configuring Google Maps and getting it working in your app, it didn’t cover creating anything very exciting. In this tutorial we’re going to use the previous example as our starting point and take the next step towards something a little more interesting: tracking location so that the map displayed makes sense contextually for your user.

Adjusting to the User’s Location

While Sydney (our current default camera location) is probably a lovely city, the first thing we’ll do is figure out where the user is and focus the camera on them. In order to get the user’s location, we will need to add a permission to the AndroidManifest.xml file within the manifest tag. We have two options here: we can request coarse location information, which is based on wifi and cellular data, or we can get fine location information based on a device’s GPS, in addition to the other two sources. For this example, let’s stick to fine location.

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

After declaring the required permission in the manifest file, we’ll need to explicitly request it in our application for devices running Android Marshmallow and above. Returning to MapsActivity.kt, go into the onMapReady() function. We’re going to update this function to check wether or not the user has already granted location permissions. If they have, enable showing their location. If they haven’t, you can prompt them to grant the permission here.

override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap

if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
initMap()
} else {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
LOCATION_PERMISSION)
}
}

You’ll notice that we have a parameter being passed into requestPermission() called LOCATION_PERMISSION. This is a value that you will set in your application to keep track of the requested permission.

private val LOCATION_PERMISSION = 42

If the user has already granted the location permission, then you can call a new method named initMap(), which will contain all of your initial map logic.

private fun initMap() {
mMap.isMyLocationEnabled = true

val sydney = LatLng(-34.0, 151.0)

mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
}

If your user hasn’t granted the location permission yet, then they will be forwarded to the onRequestPermissionResult() method, where you can check if they approved or denied it, and react accordingly.

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode,
permissions,
grantResults)
if (requestCode == LOCATION_PERMISSION) {
if (permissions.size == 1 &&
permissions[0] == Manifest.permission.ACCESS_FINE_LOCATION &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
initMap()
}
}
}

At this point if you start the app and click on the My Location button,

you should see the camera move from Sydney, Australia, to wherever you are located with a small blue dot representing your position.

Additionally, you can implement a couple of listeners that will let you know when the user has clicked on their location button, or on their current location marker (the blue dot).

class MapsActivity : AppCompatActivity(), OnMapReadyCallback,
GoogleMap.OnMyLocationButtonClickListener,
GoogleMap.OnMyLocationClickListener {

For now, let’s just have the required methods for these listeners show a Toast message.

override fun onMyLocationButtonClick(): Boolean {
Toast.makeText(this, "my location button click", Toast.LENGTH_LONG).show()
return false
}

override fun onMyLocationClick(location: Location) {
Toast.makeText(this, "my location click", Toast.LENGTH_LONG).show()
}

In order for these to be triggered, you will need to listen for the events. In initMap(), add these two lines

mMap.setOnMyLocationButtonClickListener(this)
mMap.setOnMyLocationClickListener(this)

Automatically Respond to Changes

So far we’ve made good progress in showing the user’s location on the map, but it still requires the user to actively press a button in order to center the camera on them. We can work around this by adding the location package from Google Play Services in our application. Start by going into your module’s build.gradle file and adding the following line under the dependencies node:

implementation 'com.google.android.gms:play-services-location:16.0.0'

Returning to MapsActivity.kt, create a member variable for the FusedLocationProviderClient, which is how we’ll tie into Android’s location system.

private lateinit var fusedLocationClient: FusedLocationProviderClient

This can be instantiated in onCreate() before calling getMapAsync().

fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)

Now you can remove the code in initMap() involving the marker and camera in Sydney and replace it with this code that will listen to the FusedLocationProviderClient and center the camera on the user’s currently known location.

fusedLocationClient.lastLocation
.addOnSuccessListener { location : Location? ->
for (location in locationResult.locations){
updateMapLocation(location)
}
}

where updateMapLocation() is defined as

private fun updateMapLocation(location: Location?) {
mMap.moveCamera(CameraUpdateFactory.newLatLng(LatLng(
location?.latitude ?: 0.0,
location?.longitude ?: 0.0)))

mMap.moveCamera(CameraUpdateFactory.zoomTo(15.0f))
}

After running your app, you should see that the camera automatically focuses on your location at a set level of zoom. This would be a good time to play with the zoom level and see what would work best for your use cases. For example, while a level of 15.0f is fairly close to the user’s location,

3.0f zooms out enough to show a massive portion of the Earth.

You’ll also notice that more detailed information about the user’s surrounding area comes up at closer zoom levels. In the next tutorial, we’ll cover the map’s camera in more detail.

Continuously Updating Location

One tricky thing with this setup is that the location doesn’t update as the user moves. To get around this, we can listen to a LocationCallback in order to continuously update the user’s position. At the top of MapsActivity.kt, add a new member variable for the callback

private lateinit var locationCallback: LocationCallback

This value will be initialized and applied every time we want to start listening to the user’s location, like so:

private fun initLocationTracking() {
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
locationResult ?: return
for (location in locationResult.locations){
updateMapLocation(location)
}
}
}

fusedLocationClient.requestLocationUpdates(
LocationRequest(),
locationCallback,
null)
}

You’ll notice that we aren’t just creating the callback, but we’re associating it with the FusedLocationProviderClient while also requesting updates at the end of the above function.

In order to start listening to this location updater, we will need to call our new method initLocationTracking() in both initMap() and onResume(). The reason we’re requesting locations in onResume() is we will want to stop listening for locations when the user backgrounds the app, but will also want to restart them as soon as the user returns.

override fun onResume() {
super.onResume()
if( ::mMap.isInitialized ) {
initLocationTracking()
}
}

override fun onPause() {
super.onPause()
fusedLocationClient.removeLocationUpdates(locationCallback)
}

At this point it would make sense to remove this code from the previous section around getting the device’s last known location:

fusedLocationClient.lastLocation
.addOnSuccessListener { location : Location? ->
for (location in locationResult.locations){
updateMapLocation(location)
}
}

With how this code is currently written, the camera will always refocus on the user when resuming the app or receiving a location update. This may not be the desired functionality for your own apps, so it’s worth mentioning that you can respond to changes in the user’s location in any way that makes sense for your own projects.

Conclusion

In this article you’ve learned how to update your Google Maps applications on Android to include location data about the user. By being able to show the user where they are in the world, and what’s around them, you’ll be able to give them more value from your application and help them achieve their goals. In the next article you will learn about the Google Maps camera, as well as new ways for your user’s to interact with the map. You can find the source code for this project on GitHub.