Android Kotlin - Track your location and display it in a Google Maps fragment

This tutorial will show you how you can create a Kotlin app, which detects your location through the "Location API" and display it in a "Google Maps" view.

We will use the latest stable versions, that are available during the creation of this tutorial.

 

Requirements

We will use the "Fused Location API" for the GPS location detection and "Google Maps" to display our location on a map.
Add this dependencies in your "app/build.gradle" file:

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

 

Add these permissions into your "AndroidManifest.xml" file:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<permission
   android:name="my.packagename.permission.MAPS_RECEIVE"
   android:protectionLevel="signature" />
<uses-permission android:name="my.packagename.permission.MAPS_RECEIVE" />

You have to change "my.packagename" to your package name of your "Android" project. You can see your package name in the first line of your "AndroidManifest" file.

 

You need to create an API key to use "Google Maps" in your app. Please login with your Google Account in the "Google Cloud Platform Cloud". Create a new project, if you have not done that and then go the the menu point "APIs & Services > Credentials".
Link to "Google Cloud Platform Cloud":
https://cloud.google.com/console/google/maps-apis/overview

 

Go to the page ("Credentials") and click on "Create credentials" and "API key". Your new created API key will be displayed after that. You have to add that key in a "meta" tag in the "<application>" section of your "AndroidManifest.xml" file.
It looks like this:

  <meta-data
     android:name="com.google.android.geo.API_KEY"
     android:value="XXXXXXXXXXXXXXXXXXXX" />

 

Please go again to "Google Cloud Platform Cloud" and to the menu point "Overview". Then search for "Maps SDK for Android" and click on "enable":
https://console.cloud.google.com/apis/library/maps-android-backend.googleapis.com

 

Program Code

Creating the location detection feature of our app

The application code will be created step for step and the code is shown in snippets. The complete code is available on Github.
Now we can write our application code. We will use for this example an Activity class, where the location is automatically detected and then displayed in a "Google Maps" fragment.


Please create an empty "Activity" that implements the classes "GoogleApiClient.ConnectionCallbacks" and "GoogleApiClient.OnConnectionFailedListener".
We also create the following instance variables and a static variable which will be used later in our functions:

private const val PERMISSION_REQUEST_CODE = 10

class MainActivity: AppCompatActivity(), GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {

    val API_CONNECTION_LOG_TAG = "GoogleAPIConnection"
    val GPS_LOG_TAG = "GPSDection"
    private lateinit var googleApiClient: GoogleApiClient
    private lateinit var gpsLocation: Location
    private var locationPermissions = arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION
    )
    lateinit var gpsLocationManager: LocationManager
    private lateinit var gpsGoogleMap: GoogleMap

    override fun onConnected(p0: Bundle?) {
        Log.i(API_CONNECTION_LOG_TAG, "Location services connection enabled")
        //Save GPS detection in variable
        //Display location in Google Maps Marker
    }

    override fun onConnectionSuspended(p0: Int) {
        Log.i(API_CONNECTION_LOG_TAG, "Location services disconnect. Please reconnect")
    }

    override fun onConnectionFailed(p0: ConnectionResult) {
        Log.i(API_CONNECTION_LOG_TAG, "Location services connection failed. Please reconnect")
        val connectionFailedDialog = AlertDialog.Builder(this)
        connectionFailedDialog.setTitle(getString(R.string.googleApiConnectionFailedErrorTitle))
            .setMessage(getString(R.string.googleApiConnectionFailedErrorMessage))
            .setPositiveButton(getString(R.string.googleApiConnectionFailedErrorOKButton)) { paramDialogInterface, paramInt ->
            }
        connectionFailedDialog.show()
    }
		

 

Create a function that loads the "Google API client", which will be used to detect the GPS location.

    private fun loadGoogleApiClient() {
        googleApiClient =
            GoogleApiClient.Builder(this).addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this).addApi(LocationServices.API).build()
        googleApiClient!!.connect()
    }

 

This created function will be called in the method "onCreate()":

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        loadGoogleApiClient()
        setContentView(R.layout.activity_main)
    }

This function should be called there directly after the "super" command.

As well as in the methods "onResume()". But the created "Google API client" will be disconnected in the "onPause()" method.

   override fun onResume() {
        super.onResume()
        loadGoogleApiClient()
	    requestAndLoadGpsLocation()
    }

    override fun onPause() {
        super.onPause()
        if (googleApiClient != null && googleApiClient.isConnected()) {
            googleApiClient.disconnect()
        }
    }

 

The function "requestAndLoadGpsLocation()" will be implemented later.

But now we have to create a function that checks if the user did give this app permission to access the current GPS location.
This is done through this function:

    private fun checkLocationDetectionPermission(permissionArray: Array<String>): Boolean {
        var permissionAllSuccess = true
        for (i in permissionArray.indices) {
            if (checkCallingOrSelfPermission(permissionArray[i]) == PackageManager.PERMISSION_DENIED)
                permissionAllSuccess = false
        }
        return permissionAllSuccess
    }

 

Go to your "onCreate()" function and add the following code:

	    override fun onCreate(savedInstanceState: Bundle?) {
	[ ... ]
        loadGoogleApiClient()

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkLocationDetectionPermission(locationPermissions)) {
                requestAndLoadGpsLocation()
            } else {
                requestPermissions(locationPermissions, PERMISSION_REQUEST_CODE)
            }
        } else {
            requestAndLoadGpsLocation()
        }
	[ ... ]
	}

The permission to detect the location will be checked and requested if needed. If the app does have permissions to check the location, then we will call the function "requestAndLoadGpsLocation()".

Now we need to create the function "requestAndLoadGpsLocation()":

   @SuppressLint("MissingPermission")
   fun requestAndLoadGpsLocation() {
        //If user did activate GPS, then create the locationManager that will be used to automatically detect changes in the GPS location and display the new location
        if (checkGpsActivated()) {
            try {
                gpsLocationManager.requestLocationUpdates(
                    LocationManager.GPS_PROVIDER,
                    0,
                    0F,
                    object : LocationListener {
                        override fun onLocationChanged(location: Location?) {
                            if (location != null) {
                                setNewLocation(location)
                                Log.d(
                                    GPS_LOG_TAG,
                                    "New GPS Latitude : " + location!!.latitude
                                )
                                Log.d(
                                    GPS_LOG_TAG,
                                    "New GPS Longitude : " + location!!.longitude
                                )
                            }
                        }

                        override fun onStatusChanged(
                            provider: String?,
                            status: Int,
                            extras: Bundle?
                        ) {

                        }

                        override fun onProviderEnabled(provider: String?) {

                        }

                        override fun onProviderDisabled(provider: String?) {

                        }

                    })

                //Load the GPS location
                gpsLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
                val gpsFusedLocationClient: FusedLocationProviderClient =
                    LocationServices.getFusedLocationProviderClient(this)

                gpsFusedLocationClient.getLastLocation()
                    .addOnSuccessListener(object : OnSuccessListener<Location?> {
                        override fun onSuccess(location: Location?) {
                            if (location != null) {
                                Log.d(GPS_LOG_TAG, "Location was detected")
                                Log.d(
                                    GPS_LOG_TAG,
                                    "Lat " + location.latitude + "  Long " + location.longitude
                                )
                                setNewLocation(location)
                            } else {
                                Log.d(GPS_LOG_TAG, "Location was not detected")
                            }
                        }

                    })
                    .addOnFailureListener(object : OnFailureListener {
                        override fun onFailure(e: Exception) {
                            Log.d(GPS_LOG_TAG, "Error: Trying to get last GPS location")
                            e.printStackTrace()
                        }

                    })
            } catch (e: SecurityException) {
                requestPermissions(locationPermissions, PERMISSION_REQUEST_CODE)
            }
        } else {
            errorLocationDetectionDeactivated(this)
        }
    }

If the user did activate GPS, then create the locationManager that will be used to automatically detect changes in the GPS location and display the new location. The location is request through the "Location Manager".


Now we need to create the functions, that we have called above in our new created function "requestAndLoadGpsLocation()":

Function "checkGpsActivated()":

    fun checkGpsActivated(): Boolean {
        gpsLocationManager =
            getSystemService(Context.LOCATION_SERVICE) as LocationManager
        return gpsLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
    }

 

Function "errorLocationDetectionDeactivated(mainActivity)":

    fun errorLocationDetectionDeactivated(activity: MainActivity) {
        val locationDetectionDeactivatedDialog = AlertDialog.Builder(activity)
        locationDetectionDeactivatedDialog.setTitle(getString(R.string.errorLocationDetectionDeactivatedTitle))
            .setMessage(getString(R.string.errorLocationDetectionDeactivatedMesssage))
            .setPositiveButton(getString(R.string.errorLocationDetectionDeactivatedOKButton)) { paramDialogInterface, paramInt ->
                startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
            }
        locationDetectionDeactivatedDialog.show()
    }

 

Now, we will create the functions that are neded for our "Google Maps" fragment.

Function "errorMapCouldNotBeLoaded(mainActivity)":

    fun errorMapCouldNotBeLoaded(activity: MainActivity) {
        val errorMapCouldNotBeLoadedDialog = AlertDialog.Builder(activity)
        errorMapCouldNotBeLoadedDialog.setTitle(getString(R.string.errorMapCouldNotBeLoadedTitle))
            .setMessage(getString(R.string.errorMapCouldNotBeLoadedMessage))
            .setPositiveButton(getString(R.string.errorMapCouldNotBeLoadedOKButton)) { paramDialogInterface, paramInt ->
                startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
            }
        errorMapCouldNotBeLoadedDialog.show()
    }

 

The function "setNewLocation(location)" that saves our new location and updates it in the "Google Maps" fragment:

 fun setNewLocation(
        location: Location
    ) {
        if (::gpsGoogleMap.isInitialized && gpsGoogleMap != null) {
            val gpsPositionLatLng = LatLng(location.latitude, location.longitude)
            latitudeValue.setText(String.format("%.5f", location.latitude))
            longitudeValue.setText(String.format("%.5f", location.longitude))

            gpsGoogleMap.clear()
            gpsGoogleMap.addMarker(
                MarkerOptions().position(gpsPositionLatLng)
                    .title(getString(R.string.my_position_text) + gpsPositionLatLng.latitude + " - " + gpsPositionLatLng.longitude)
            )

            gpsGoogleMap.moveCamera(
                CameraUpdateFactory.newLatLngZoom(
                    gpsPositionLatLng,
                    20F
                )
            )

        } else {
            Log.d("MapDisplay", "Map could not be loaded")
            errorMapCouldNotBeLoaded(this)
        }
    }

The function "moveCamera()" moves the "Google Maps" view to the langitude and latitude that is defined in the the required "LatLng" variable "gpsPositionLatLng". The "Google Maps" view is zoomed to the size "20F", which can be any "float" value between 1 and 21.
If you want to have an zoom animation into your position, then you can use the function "animateCamera()" with the same values.
The command "isInitialized" checks if the object was created.

We did add a function which does ask for permission to detect the location by calling the permission request dialog from the Android operating system.

But now we need the add a function which will define what the application will do after the display of the permission request dialog.

Function "onRequestPermissionsResult()":

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {

        if (requestCode == PERMISSION_REQUEST_CODE && grantResults.size > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            requestAndLoadGpsLocation()
            return
        }
        //Display error if permission was not granted or the gps is deactivated
        fun errorLocationDetectionDeactivated(activity: MainActivity) {
            val locationDetectionDeactivatedDialog = AlertDialog.Builder(activity)
            locationDetectionDeactivatedDialog.setTitle(getString(R.string.errorLocationDetectionDeactivatedTitle))
                .setMessage(getString(R.string.errorLocationDetectionDeactivatedMesssage))
                .setPositiveButton(getString(R.string.errorLocationDetectionDeactivatedOKButton)) { paramDialogInterface, paramInt ->
                    startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
                }
            locationDetectionDeactivatedDialog.show()
        }

    }

We will call our location detection function only, if the user did grant the required permission. If that is not the case, then we will display an error dialog instead.

Set up of the Map that displays our location

Now we reach the last part of this tutorial.

You have to implement the interface "OnMapReadyCallback" in the MainActivity and add the method "OnMapReady()" with the following code:

class MainActivity : AppCompatActivity(),
    GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener,
    OnMapReadyCallback {

	[...]
	
    override fun onMapReady(map: GoogleMap?) {
        if (map != null) {
            loadMap(map)
        } else {
            errorMapCouldNotBeLoaded(this)
        }
    }

   fun loadMap(map: GoogleMap) {
        gpsGoogleMap = map
        gpsGoogleMap.mapType = GoogleMap.MAP_TYPE_NORMAL
    }
	
}	

If you want to use the satellite version of "Google Maps", then use the map type "GoogleMap.MAP_TYPE_SATELLITE". The map type "GoogleMap.MAP_TYPE_HYBRID" displays both the satellite version and all the labeling (streets, city names, etc.) from the card version.

Now we can create the layout of our "activity". Please add the following to your file "layout_main.xml":

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:id="@+id/latlngInfoLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal"
            android:padding="15dp"
            android:weightSum="10">

            <TextView
                android:id="@+id/latitudeDesc"
                android:layout_width="50dp"
                android:layout_height="wrap_content"
                android:layout_weight="2"
                android:text="@string/latDesc"
                android:textColor="@android:color/black" />

            <TextView
                android:id="@+id/latitudeValue"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="8"
                android:textColor="@android:color/black" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="10"
            android:orientation="horizontal"
            android:padding="15dp"
            android:weightSum="10"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <TextView
                android:id="@+id/longitudeDesc"
                android:layout_width="50dp"
                android:layout_height="wrap_content"
                android:layout_weight="2"
                android:text="@string/longDesc"
                android:textColor="@android:color/black" />

            <TextView
                android:id="@+id/longitudeValue"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="8"
                android:textColor="@android:color/black" />
        </LinearLayout>

    </LinearLayout>

    <fragment
        android:id="@+id/gpsMapView"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="409dp"
        android:layout_height="694dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/latlngInfoLayout"
        tools:context=".MainActivity" />

</androidx.constraintlayout.widget.ConstraintLayout>

You need to use a "fragment" to display the "Google Maps" view.

Finally, we have to load the new created "fragment". You have to add this code to your "onCreate()" in your "activity":

 val gpsMapFragment = supportFragmentManager.findFragmentById(R.id.gpsMapView) as SupportMapFragment
 gpsMapFragment.getMapAsync(this)

 

This was an example on how to use the Location API to detect your GPS location. You can now compile your application.
The detection of your location could take some time, when you start the app for the first time. This application with the complete code is also available on "Github".

This application on "Github":
https://github.com/a-dridi/LocationDetection

More about "Google Maps API":
https://cloud.google.com/console/google/maps-apis/overview

More about "Location API":
https://developer.android.com/training/location

Category: