Everything you need to know about using Google Maps in Cab Booking App (Part 1)

Hey, Android Developers! I would like to share my experience with you guys when I got to make Cab booking application. I learn a lot during the development of this application. The following are the features which we’re going to discuss in this article:

  • Show GoogleMaps.
  • Animate multiples marker simultaneously on GoogleMaps.
  • Calculate the distance from the current location to the nearest driver.
  • Show multiples online drivers on GoogleMaps.
  • Animate the User PinView and show nearest driver distance time.
  • Working with Firebase Real-time Database.
  • Reverse Geocode location where the user drags current Marker.

What our app will be?

To show you how to implement above mention features in an app, we’re going to create a simple app called Frisbee: it will start listening to drivers from firebase realtime database and show the online driver in the user application. Animate the multiple driver markers simultaneously whenever their current location changed in firebase realtime database. Calculate the distance from user current location to the nearest driver, animate the Marker while calculating the distance and show the result inside the custom Marker.

Before to start coding the Frisbee, I want to show you guys the demo of application:

Prerequisite

  • Requires Google Maps API key to show Google Maps. Go to Developer Console -> Sign In with your Gmail account -> Create a new project -> API & Services -> Library -> Search for Google Maps and click on Maps SDK for Android -> Enable API. After enabling the Maps SDK, go to API & Services -> Credentials -> Create Credentials -> API key -> Restrict API key for your Android project.
  • Google Distance Matrix API key to calculate distance. Go to the previously created project in Developer Console -> API & Services -> Library -> Search for Distance Matrix and click on Distance Matrix API -> Enable API. After enabling the Distance Matrix API, go to API & Services -> Credentials -> Create Credentials -> API key -> Restrict key, change the name (optional), and finally click on save button. To add restrictions to an API key see this link.

Note: This tutorial will not focus on how to build the app for scale or for performance. It’s basically designed so that you can have fun while building it, and how you can create something that mimics Uber or Careem. Think of this as though building a Minimum Viable Product to demonstrate your idea or startup, for a proof-of-concept.

Now that everything is done, let’s make our hands dirty and create a new Android Studio Empty project:

Android Studio

In Android Studio go to Create Android Project, press next twice and select Empty Activity, then finish. After the Android Studio gradle builds successfully you’re ready to do the code. You can see MainActivity like below:

import android.support.v7.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

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

Before start coding our application we need to add some dependencies to our app level build.gradle file. The following shows the dependencies:

// Google dependencies
implementation 'com.google.android.gms:play-services-maps:16.0.0'
implementation 'com.google.firebase:firebase-database:16.0.5'
implementation 'com.google.android.gms:play-services-location:16.0.0'

// Material dialog dependency
implementation 'com.afollestad.material-dialogs:core:0.9.6.0'

// ViewModel dependencies
implementation 'android.arch.lifecycle:viewmodel:1.1.1'
implementation 'android.arch.lifecycle:extensions:1.1.1'

// Kotlin coroutines dependencies
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1'

// Map services dependency
implementation 'com.google.maps:google-maps-services:0.2.4'

Sync the project and everything should be fine without any kind of gradle error.


Let’s make our activity_main.xml file to show the views on the screen.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/mainActivityRootView"
        tools:context=".activities.main.ui.MainActivity">
    
    // 1
    <fragment xmlns:tools="http://schemas.android.com/tools"
              android:id="@+id/map"
              android:name="com.google.android.gms.maps.SupportMapFragment"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              tools:context="spartons.com.frisbee.activities.main.ui.MainActivity"/>

    // 2
    <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true">

        <FrameLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginBottom="100dp"
                tools:ignore="UselessParent">

            <FrameLayout
                    android:id="@+id/pinViewLine"
                    android:layout_width="2dp"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:layout_marginTop="40dp"
                    android:background="@drawable/line_background"/>

            <FrameLayout
                    android:id="@+id/pinViewCircle"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:background="@drawable/circle_background">

                <TextView
                        android:id="@+id/pinTimeTextView"
                        android:layout_width="40dp"
                        android:layout_height="45dp"
                        android:layout_gravity="top|center_horizontal"
                        android:gravity="center"
                        android:padding="2dp"
                        android:textColor="@android:color/white"
                        android:textSize="12sp"/>

                <ProgressBar
                        android:id="@+id/pinProgressLoader"
                        android:layout_width="45dp"
                        android:layout_height="45dp"
                        android:layout_gravity="top|center_horizontal"
                        android:layout_margin="4dp"
                        android:contentDescription="@null"
                        android:indeterminate="true"
                        android:visibility="gone"/>

            </FrameLayout>

        </FrameLayout>

    </FrameLayout>

    //  3
    <TextView
            android:id="@+id/currentPlaceTextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerInParent="true"
            android:ellipsize="end"
            android:gravity="center"
            android:maxLines="1"
            android:background="@drawable/place_search_style"
            android:layout_margin="30dp"
            android:padding="10dp"
            android:text="@string/search_place"
            android:textColor="@color/colorPrimaryText"/>

    // 4

    <ImageButton
            android:id="@+id/currentLocationImageButton"
            android:layout_width="40dp"
            android:layout_height="wrap_content"
            android:layout_above="@+id/currentPlaceTextView"
            android:layout_alignParentEnd="true"
            android:layout_marginBottom="10dp"
            android:layout_marginEnd="2dp"
            android:contentDescription="@null"
            android:src="@drawable/current_location_icon"
            android:layout_alignParentRight="true"
            android:layout_marginRight="2dp"/>

</RelativeLayout>

Below is the explanation of the above code:

  1. The fragment tag to show the Google Maps.
  2.  FrameLayout for the custom Marker. The custom Marker is for a user to drag anywhere on the Google Map and set the pick-up location for a ride.
  3.  TextView for the current location place name.
  4. Finally, the ImageButton is for to moves the camera to a user’s current location.

Build your application and run to view your progress so far.

Custom Google Map MarkerThere are many key points in this application. So, we’re gonna need to break the application into smaller parts.

I will try to explain as many of lines of code as I can. You can follow looking on commits of code that I published on GitHub. Java application link and Kotlin application link.

1. Initialize The Google Maps

In order to use Google Maps, we need to initialize it first in our MainActivity onCreate method and store the instance of Google Maps so that later we can use it.

class MainActivity : AppCompatActivity() {

    private var googleMap : GoogleMap? = null

    override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
           .......
           .......

          val supportMapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
          supportMapFragment.getMapAsync { googleMap ->
                this.googleMap = googleMap
          }
    }

}

2. Animate Google Maps to user current location:

In this part of an application, we’re going to request user current location from LocationServices and animate the Google Map to that location. Before requesting the last known location from a device we need to check that if the user has PlayServices installed in his/her mobile. Add the below code in MainActivity onCreate method:

if(!uiHelper.isPlayServicesAvailable()) {
     toast("Play Services is not installed!")
     finish()
} else requestLocationUpdates()

If the user has not installed PlayServices in his/her mobile phone then we’re gonna exit the application and show Toast else, we simply request location updates from the device. The UiHelper is the utility class with just a bunch of helper method.

Next, add the following code inside the UiHelper class.

fun isPlayServicesAvailable() : Boolean {
    val googleApiAvailability = GoogleApiAvailability.getInstance()
    val status = googleApiAvailability.isGooglePlayServicesAvailable(context)
    return ConnectionResult.SUCCESS == status
}

As you may have noticed that if the user has PlayServices installed then we’re calling our requestLocationUpdates method. Add the below method inside MainActivity.

private fun requestLocationUpdates() {
        if (!uiHelper.isHaveLocationPermission()) {
            ActivityCompat.requestPermissions(
                this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                MY_PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION
            )
            return
        }
        viewModel.requestLocationUpdates()
    }

The code above checks if the app has been granted the ACCESS_FINE_LOCATION permission. If it hasn’t, then request it from the user because, starting with Android 6.0, user permission is handled little differently than before. You don’t request permission during the installation of your app; rather, you request them at runtime when the permission is actually required.

If the user granted the location permission then simply we ‘re gonna call our requestLocationUpdates method inside the ViewModel. All of our network calls and logical stuff will be handled inside MainActivityViewModel and listens to the data via LiveData.

You can check out my other articles on LiveData and ViewModel.

Add the below method inside MainActivityViewModel class.

fun requestLocationUpdates() {
   locationProviderClient.requestLocationUpdates(uiHelper.getLocationRequest(), locationCallback, Looper.myLooper());
}

The FusedLocationProviderClient request location updates with LocationCallback on a specified Looper. Also, you may have noticed that we need a locationCallback and  LocationRequest to successfully receive location updates.

Add the following getLocationRequest method inside the UiHelper class.

fun getLocationRequest() : LocationRequest {
   val locationRequest = LocationRequest.create()
   locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
   locationRequest.interval = 3000
   return locationRequest
}

The LocationRequest object is usually used to request a quality of service for location updates. You see we’re setting high priority accuracy and location interval to 3 seconds. Now after every 3 seconds our application will receive a Location update via LocationCallback which we’ve passed previously when requesting location updates.

After adding LocationRequest we only need to create our LocationCallback instance. Add the LocationCallback object creation as follows inside MainActivityViewModel class.

private val locationCallback: LocationCallback = object : LocationCallback() {

    override fun onLocationResult(locationResult: LocationResult?) {
        super.onLocationResult(locationResult)
        if (locationResult?.lastLocation == null) return
        _currentLocation.postValue(locationResult.lastLocation)
    }
}

The onLocationResult method called immediately when the device location information is available. The other thing we’re doing in this method is sending the location back to our MainActivity via LiveData.

Before start observing the current location in MainActivity we need to define this _currentLocation LiveData inside our MainActivityViewModel class.

private val _currentLocation = MediatorLiveData<Location>()

val currentLocation: LiveData<Location> get() = _currentLocation

You see we’re not exposing our MediatorLiveData instance publically instead we simply given a LiveData just to observe the location. By doing this we’re keeping our immutability principle safe.

Now that we’ve successfully requested location updates. Go to MainActivity and paste the following code inside the onCreate method to observe the user’s current location from MainActivityViewModel.

viewModel.currentLocation
         .observe(this, Observer<Location> { location ->
               if (firstTimeFlag) {
                        firstTimeFlag = false;
                        animateCamera(location);
               }
               showOrAnimateMarker(location);         
         })

The Observer interface is immediately invoked providing the most recent Location stored in currentLocation. The firstTimeFlag is our globally initialized bool, a default set to true. The reason we’re checking the firstTimeFlag condition, is because we need to animate the Google Maps camera to user current location when a user opens the application only the first time. After that, we simply animate the user current location Marker from previous coordinates to new coordinates.

Add the below animateCamera method inside MainActivity class.

private fun animateCamera(location : Location) {
     val cameraUpdate = googleMapHelper.buildCameraUpdate(location)
     googleMap.animateCamera(cameraUpdate, 10, null)
}

The animateCamera method animates the camera to the user’s current location. The GoogleMapHelper class provides several utility methods (CameraUpdate, default map settings, UserMarker) for Google Maps.

Next, add the following buildCameraUpdate method inside GoogleMapHelper class.

fun buildCameraUpdate(location: Location): CameraUpdate {
    val cameraPosition = CameraPosition.Builder()
        .target(LatLng(location.latitude, location.longitude))
        .tilt(TILT_LEVEL.toFloat())
        .zoom(ZOOM_LEVEL.toFloat())
        .build()
    return CameraUpdateFactory.newCameraPosition(cameraPosition)
}

Now add the showOrAnimateMarker method in the MainActivity class.

private fun showOrAnimateMarker(Location location) {
     if(currentLocationMarker == null){
           currentLocationMarker = googleMap.addMarker(googleMapHelper.getUserMarker(location))
     } else {
         MarkerAnimationHelper.animateMarkerToGB(currentLocationMarker, location, LatLngInterpolator.Spherical())
     }
}

You see first we check if the currentLocatinMarker is null then we create a new MarkerOptions with the help of GoogleMapHelper utility class, add Marker inside googleMap and store the Marker instance in the currentLocationMarker. Afterward, if it is not null then we simply animate the currentLocationMarker to user new location.

The LatLngInterpolator and MarkerAnimationHelper class are same which I explain in my previous article.

Build and run to view your progress so far. You should see a pin on your location and also your blue dot location Marker will animate if you’re traveling, walking, running etc.

Google Map Current Location MarkerNow that everything is done for showing the current location on Google Map and animate the Marker with user current location let’s start building our third application part.

3. Reverse Geocode PinView Coordinates And Show PlaceName

In order to Reverse Geocode the coordinates, we need to have user PinView Location. We can easily get these coordinates from CameraPosition target method. The target method will always give us the center point coordinates of Google Map which we see on screen and our PinView also stays in the middle of the screen. Now if we get the center point coordinates from target method, it means we’re getting the PinView coordinates.

Another thing we need to discuss is this, where do we call our Reverse Geocode method? If you guys have clearly see the demo of application you’ll understand that as soon the Google Map camera movement stopped the place name appeared inside the TextView. So, for this, we can easily implement OnCameraIdleListener on MainActivity. The OnCameraIdleListener has its callback function which defines what actions needs to done when the camera stopped its movement. In this callback, we need to get the coordinates from PinView and execute our Reverse Geocode request.

Let’s start the third part of an application by implementing the OnCameraIdleListener on MainActivity.

class MainActivity : AppCompatActivity(), GoogleMap.OnCameraIdleListener {

      private var googleMap : GoogleMap? = null

      override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            
            .......
            .......

            val supportMapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment supportMapFragment.getMapAsync { googleMap ->
                    googleMap.setOnCameraIdleListener(this)                          
                    this.googleMap = googleMap 
            }
     }

Now in onCameraIdle method, we need to get the current camera target coordinates from Google Maps and call the Reverse Geocode method.

override fun onCameraIdle() {
    val position = googleMap.cameraPosition.target
    viewModel.makeReverseGeocodeRequest(position, geoCoderValue.value); 
}

I know you guys must be thinking what is geoCoderValue. The geoCoderValue is nothing just a lazy initialized GeoCoder instance. It’s a good approach that we only initialized our GeoCoder once instead of creating a new instance every time. Add the below code inside the MainActivity to create lazy GeoCoder.

private val geoCoderValue = lazy {
      Geocoder(this)
  }

After creating GeoCoder inside MainActivity add the following code inside the MainActivityViewModel class to Reverse Geocode the PinView coordinates:

fun makeReverseGeocodeRequest(latLng: LatLng, geoCoder: Geocoder) {

    //1
    launch(context = coroutineContext) {

        // 2
        val result = geoCoder.getFromLocation(latLng.latitude, latLng.longitude, 1)
        if (result != null && result.size > 0) {
            val address = result[0]

             // 3
            _reverseGeocodeResult.postValue(address.getAddressLine(0).plus(" , ").plus(address.locality))
        }
    }
}

Here’s what’s going on in the makeReverseGeocodeRequest  method:

  1.  launch a coroutine builder so that our called thread will not be blocked. The Google documentation recommend us to Reverse Geocode in a separate thread instead of UI thread.
  2.  The getFromLocation method returns an array of Addresses that are known to describe the area.
  3. If the GeoCoder results are not null then we pass the result to MainActivity via LiveData to show the result in the TextView.

Before going to MainActivity and start observing the result we need to create our _reverseGeocodeResult LiveData inside MainActivityViewModel class.

private val _reverseGeocodeResult = MediatorLiveData<String>()

val reverseGeocodeResult : LiveData<String> = _reverseGeocodeResult

Now that we’ve successfully got the place name from coordinates. Go to MainActivity and paste the following code inside the onCreate method to observe the result from MainActivityViewModel.

viewModel.reverseGeocodeResult
            .observe(this, Observer<String> { placeName
                currentPlaceTextView.text = placeName
            })

When the Observer interface invoked we simply set the placeName in the TextView.

As a result, run the application you’ll see that the place name will be updated when you drag the PinView.

Reverse Geocode And Marker AnimationAlright, guys, that was all for this article. In the next article, we’ll see how to read online drivers from firebase realtime database and show or animate multiple Markers simultaneously.

Thank you for being here and keep reading…

 

3 COMMENTS

LEAVE A REPLY

Please enter your comment!
Please enter your name here