We are committed to provide service 24 X 7

Deals, Shopping, Training, Tools

Learn how to – Android: Select location on the map using draggable markers and Google Search API

Learn how to – Android: Select location on the map using draggable markers and Google Search API.

When developing a map focused app in Android, sometimes you will want your users to select an accurate location. Mostly you will use Google Search API, which is robust really. However, if you take a closer look at Google map markers, you will realize that they can be dragged to a precise location.

In this tutorial, I will continue with the previous project where I explained how to show the current user location on Google map. In that project, I talked on map initialization and basic Android Google Maps knowledge. In this project, you will be required to enable Google Search API from the Google console following the same steps in enabling Google Maps API. Please note that you don’t have to have a new API key, using the same as the Maps’ key is just enough.

You can find this project from my GitHub. Clone or download to follow along.

Open build.gradle(Module: app) file and these dependencies.

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

Great! To start with I will try to create an Uber clone for selecting locations. We will have a starting point and a destination. More like calling for a ride, you will have the start point and destination with a map. Here is the XML file for my main activity.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    app_layout_behavior="@string/appbar_scrolling_view_behavior"
    tools_context=".MainActivity"
    tools_showIn="@layout/activity_main">
    <fragment
        class="com.google.android.gms.maps.SupportMapFragment"
        android_name="com.google.android.gms.maps.SupportMapFragment"
        android_id="@+id/map"
        android_layout_width="match_parent"
        android_layout_height="match_parent" />
    <com.google.android.material.card.MaterialCardView
        android_layout_width="match_parent"
        android_layout_height="wrap_content"
        android_layout_margin="@dimen/activity_margin"
        app_cardUseCompatPadding="true"
        app_cardBackgroundColor="@android:color/white"
        app_cardCornerRadius="2dp">
        <LinearLayout
            android_layout_width="match_parent"
            android_layout_height="wrap_content"
            android_layout_margin="8dp"
            android_orientation="vertical">
            <LinearLayout
                android_layout_width="match_parent"
                android_layout_height="wrap_content"
                android_orientation="horizontal">
                <androidx.appcompat.widget.AppCompatImageView
                    android_layout_width="24dp"
                    android_layout_height="24dp"
                    android_layout_gravity="center"
                    android_src="@drawable/ic_pickup"/>
                <androidx.appcompat.widget.AppCompatTextView
                    android_id="@+id/fromLocationTxt"
                    android_layout_width="0dp"
                    android_layout_height="wrap_content"
                    android_layout_weight="1"
                    android_layout_gravity="center"
                    android_textColor="@color/textColorPrimary"
                    android_text="@string/from_current_location_empty"
                    android_maxLines="1"
                    android_ellipsize="end"/>
                <com.google.android.material.button.MaterialButton
                    android_id="@+id/setStartPoint"
                    android_layout_width="wrap_content"
                    android_layout_height="wrap_content"
                    android_textAllCaps="false"
                    android_text="@string/set_location"
                    style="@style/Widget.MaterialComponents.Button.TextButton"/>
            </LinearLayout>
            <LinearLayout
                android_layout_width="match_parent"
                android_layout_height="wrap_content"
                android_orientation="horizontal">
                <androidx.appcompat.widget.AppCompatImageView
                    android_layout_width="24dp"
                    android_layout_height="24dp"
                    android_layout_gravity="center"
                    android_src="@drawable/drop_pin"/>
                <androidx.appcompat.widget.AppCompatTextView
                    android_id="@+id/toLocationTxt"
                    android_layout_width="0dp"
                    android_layout_height="wrap_content"
                    android_layout_weight="1"
                    android_layout_gravity="center"
                    android_textColor="@color/textColorPrimary"
                    android_text="@string/to_location_empty"
                    android_maxLines="1"
                    android_ellipsize="end"/>
                <com.google.android.material.button.MaterialButton
                    android_id="@+id/setFinishPoint"
                    android_layout_width="wrap_content"
                    android_layout_height="wrap_content"
                    android_textAllCaps="false"
                    android_text="@string/set_location"
                    style="@style/Widget.MaterialComponents.Button.TextButton"/>
            </LinearLayout>
            <androidx.appcompat.widget.AppCompatTextView
                android_layout_width="match_parent"
                android_layout_height="wrap_content"
                android_textSize="12sp"
                android_textStyle="italic"
                android_textColor="@color/textColorPrimary"
                android_text="@string/drag_info"/>
        </LinearLayout>
    </com.google.android.material.card.MaterialCardView>
</RelativeLayout>

On the main activity or your map activity, we initialize the Google map, fusedLocationClient, and markers. Notice also that I have a variable searchType to hold the type of search; whether starting location or destination.

private lateinit var googleMap: GoogleMap
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
private lateinit var startMarker: Marker
private lateinit var finishMarker: Marker
private var searchType: Int = 0 

Most of the functions and methods were already discussed in my previous article on showing the current location of the user so I won’t go over it. So the idea is to have a starting point as your current location with the option of changing it to another location either by dragging the marker or searching.

The onMapReady() method initializes the map and places the user on their current location on the map.

override fun onMapReady(map: GoogleMap?) {
    googleMap = map?: return
    if (isPermissionGiven()){
        googleMap.isMyLocationEnabled = true
        googleMap.uiSettings.isMyLocationButtonEnabled = true
        googleMap.uiSettings.isZoomControlsEnabled = true
        getCurrentLocation()
    } else {
        givePermission()
    }
    googleMap.setOnMarkerDragListener(this)
}

We use the location request to continuously refresh the user’s current location.

private fun getCurrentLocation() {
    val locationRequest = LocationRequest()
    locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
    locationRequest.interval = (10 * 1000).toLong()
    locationRequest.fastestInterval = 2000
    val builder = LocationSettingsRequest.Builder()
    builder.addLocationRequest(locationRequest)
    val locationSettingsRequest = builder.build()
    val result = LocationServices.getSettingsClient(this).checkLocationSettings(locationSettingsRequest)
    result.addOnCompleteListener { task ->
        try {
            val response = task.getResult(ApiException::class.java)
            if (response!!.locationSettingsStates.isLocationPresent){
                getLastLocation()
            }
        } catch (exception: ApiException) {
            when (exception.statusCode) {
                LocationSettingsStatusCodes.RESOLUTION_REQUIRED -> try {
                    val resolvable = exception as ResolvableApiException
                    resolvable.startResolutionForResult(this, REQUEST_CHECK_SETTINGS)
                } catch (e: IntentSender.SendIntentException) {
                } catch (e: ClassCastException) {
                }
                LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE -> { }
            }
        }
    }
}

We now use the FusedLocationClient to retrieve the user’s current location

private fun getLastLocation() {
    fusedLocationProviderClient.lastLocation
        .addOnCompleteListener(this) { task ->
            if (task.isSuccessful && task.result != null) {
                val mLastLocation = task.result
                setStartLocation(mLastLocation!!.latitude, mLastLocation.longitude, "")
            } else {
                Toast.makeText(this, "No current location found", Toast.LENGTH_LONG).show()
            }
        }
}
Set the user’s starting point and add marker to it.
private fun setStartLocation(lat: Double, lng: Double, addr: String){
    var address = "Current address"
    if (addr.isEmpty()){
        val gcd = Geocoder(this, Locale.getDefault())
        val addresses: List<Address>
        try {
            addresses = gcd.getFromLocation(lat, lng, 1)
            if (addresses.isNotEmpty()) {
                address = addresses[0].getAddressLine(0)
            }
        } catch (e: IOException) {
            e.printStackTrace()
        }
    } else {
        address = addr
    }
    val icon = BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(this.resources, R.drawable.ic_pickup))
    startMarker = googleMap.addMarker(
        MarkerOptions()
            .position(LatLng(lat, lng))
            .title("Start Location")
            .snippet("Near $address")
            .icon(icon)
            .draggable(true)
    )
    val cameraPosition = CameraPosition.Builder()
        .target(LatLng(lat, lng))
        .zoom(17f)
        .build()
    googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
    fromLocationTxt.text = String.format("From: Near %s", address)
}

The most notable point here is the draggable(true) on the MarkerOptions() and the initialization the startMarker to the map. Setting draggable to true makes the marker draggable and the vice versa is true. Now with this, after the marker is shown, you can long-press the marker then drag it to your preferred location. Also, note that that this is possible because if you can see on the map initialization we added a marker drag listener:

googleMap.setOnMarkerDragListener(this)

Now to get the coordinates of the new location, we will use the listener functions. We have three of them:

override fun onMarkerDragEnd(p0: Marker?) {
    if (p0 == startMarker){
        setStartLocation(p0.position.latitude, p0.position.longitude, "")
    } else if (p0 == finishMarker){
        setFinishLocation(p0.position.latitude, p0.position.longitude, "")
    }
}
override fun onMarkerDragStart(p0: Marker?) {
    Toast.makeText(this, "Changing location", Toast.LENGTH_SHORT).show()
}
override fun onMarkerDrag(p0: Marker?) {}

onMarkerDragEnd() will return the marker. From the marker, we can get the position and use the Geocoder to try and get the address of the place. This function is implemented in the setStartLocation() function. It is that simple!

Google Maps Search API

This is a simple invocation of google search function so that the user can input their desired location or point. When using this API, please take note of the billing from Google!

In the new versions of the API, you are required to first initialize it. Then add fields. This initialization is mostly done in your application class. Here is where we invoke it:

if (!Places.isInitialized()) {
    Places.initialize(applicationContext, BuildConfig.ApiKeyMap)
}
val fields = listOf(Place.Field.ID, Place.Field.NAME, Place.Field.LAT_LNG)
val intent = Autocomplete.IntentBuilder(AutocompleteActivityMode.FULLSCREEN, fields).build(this)
if (searchType == 1){
    startActivityForResult(intent, CURRENT_PLACE_AUTOCOMPLETE_REQUEST_CODE)
} else {
    startActivityForResult(intent, DESTINATION_PLACE_AUTOCOMPLETE_REQUEST_CODE)
}

In the intentBuilder, you have two options, AutocompleteActivityMode.FULLSCREEN or AutocompleteActivityMode.OVERLAY

Try them out and
choose the one that suits you.

Next is the onActivityResult data. We extract the location from the intent data and send them to the corresponding methods.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == CURRENT_PLACE_AUTOCOMPLETE_REQUEST_CODE){
        when (resultCode) {
            AutocompleteActivity.RESULT_OK -> {
                val place = Autocomplete.getPlaceFromIntent(data!!)
                val latLng = place.latLng.toString()
                val location = latLng.substring(latLng.indexOf("(") + 1, latLng.indexOf(")"))
                val loc = location.split(",")
                setStartLocation(loc[0].toDouble(), loc[1].toDouble(), place.name!!)
            }
            AutocompleteActivity.RESULT_ERROR -> {
                val status = Autocomplete.getStatusFromIntent(intent)
                Toast.makeText(this, status.statusMessage, Toast.LENGTH_SHORT).show()
            }
            AutocompleteActivity.RESULT_CANCELED -> Toast.makeText(this, getString(R.string.request_cancelled), Toast.LENGTH_SHORT).show()
        }
    } else if (requestCode == DESTINATION_PLACE_AUTOCOMPLETE_REQUEST_CODE){
        when (resultCode) {
            AutocompleteActivity.RESULT_OK -> {
                val place = Autocomplete.getPlaceFromIntent(data!!)
                val latLng = place.latLng.toString()
                val location = latLng.substring(latLng.indexOf("(") + 1, latLng.indexOf(")"))
                val loc = location.split(",")
                setFinishLocation(loc[0].toDouble(), loc[1].toDouble(), place.name!!)
            }
            AutocompleteActivity.RESULT_ERROR -> {
                val status = Autocomplete.getStatusFromIntent(intent)
                Toast.makeText(this, status.statusMessage, Toast.LENGTH_SHORT).show()
            }
            AutocompleteActivity.RESULT_CANCELED -> Toast.makeText(this, getString(R.string.request_cancelled), Toast.LENGTH_SHORT).show()
        }
    } else if (requestCode == REQUEST_CHECK_SETTINGS){
        if (resultCode == Activity.RESULT_OK) {
            getCurrentLocation()
        }
    }
}

It is just that simple! Here is the full activity:

package com.learnhowto.trucksend
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.IntentSender
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.location.Address
import android.location.Geocoder
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.core.app.ActivityCompat
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.common.api.ResolvableApiException
import com.google.android.gms.location.*
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.*
import com.google.android.libraries.places.api.Places
import com.google.android.libraries.places.api.model.Place
import com.google.android.libraries.places.widget.Autocomplete
import com.google.android.libraries.places.widget.AutocompleteActivity
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
import com.karumi.dexter.Dexter
import com.karumi.dexter.PermissionToken
import com.karumi.dexter.listener.PermissionDeniedResponse
import com.karumi.dexter.listener.PermissionGrantedResponse
import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.single.PermissionListener
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.content_main.*
import java.io.IOException
import java.util.*
class MainActivity : AppCompatActivity(), OnMapReadyCallback, PermissionListener,
    GoogleMap.OnMarkerDragListener {
    companion object {
        const val REQUEST_CHECK_SETTINGS = 43
        const val CURRENT_PLACE_AUTOCOMPLETE_REQUEST_CODE = 53
        const val DESTINATION_PLACE_AUTOCOMPLETE_REQUEST_CODE = 63
    }
    private lateinit var googleMap: GoogleMap
    private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
    private lateinit var startMarker: Marker
    private lateinit var finishMarker: Marker
    private var searchType: Int = 0 //0->current location, 1->search start location, 2->search destination
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment?
        mapFragment!!.getMapAsync(this)
        fusedLocationProviderClient = FusedLocationProviderClient(this)
        setStartPoint.setOnClickListener {
            searchType = 1
            givePermission()
        }
        setFinishPoint.setOnClickListener {
            searchType = 2
            givePermission()
        }
    }
    override fun onMapReady(map: GoogleMap?) {
        googleMap = map?: return
        if (isPermissionGiven()){
            googleMap.isMyLocationEnabled = true
            googleMap.uiSettings.isMyLocationButtonEnabled = true
            googleMap.uiSettings.isZoomControlsEnabled = true
            getCurrentLocation()
        } else {
            givePermission()
        }
        googleMap.setOnMarkerDragListener(this)
    }
    override fun onMarkerDragEnd(p0: Marker?) {
        if (p0 == startMarker){
            setStartLocation(p0.position.latitude, p0.position.longitude, "")
        } else if (p0 == finishMarker){
            setFinishLocation(p0.position.latitude, p0.position.longitude, "")
        }
    }
    override fun onMarkerDragStart(p0: Marker?) {
        Toast.makeText(this, "Changing location", Toast.LENGTH_SHORT).show()
    }
    override fun onMarkerDrag(p0: Marker?) {}
    private fun isPermissionGiven(): Boolean{
        return ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
    }
    private fun givePermission() {
        Dexter.withActivity(this)
            .withPermission(Manifest.permission.ACCESS_FINE_LOCATION)
            .withListener(this)
            .check()
    }
    override fun onPermissionGranted(response: PermissionGrantedResponse?) {
        if (searchType == 0){
            getCurrentLocation()
        } else {
            Toast.makeText(this, resources.getString(R.string.loading), Toast.LENGTH_LONG).show()
            if (!Places.isInitialized()) {
                Places.initialize(applicationContext, BuildConfig.ApiKeyMap)
            }
            val fields = listOf(Place.Field.ID, Place.Field.NAME, Place.Field.LAT_LNG)
            val intent = Autocomplete.IntentBuilder(AutocompleteActivityMode.OVERLAY, fields).build(this)
            if (searchType == 1){
                startActivityForResult(intent, CURRENT_PLACE_AUTOCOMPLETE_REQUEST_CODE)
            } else {
                startActivityForResult(intent, DESTINATION_PLACE_AUTOCOMPLETE_REQUEST_CODE)
            }
        }
    }
    override fun onPermissionRationaleShouldBeShown(
        permission: PermissionRequest?,
        token: PermissionToken?
    ) {
        token!!.continuePermissionRequest()
    }
    override fun onPermissionDenied(response: PermissionDeniedResponse?) {
        Toast.makeText(this, "Permission required for showing location", Toast.LENGTH_LONG).show()
        finish()
    }
    private fun getCurrentLocation() {
        val locationRequest = LocationRequest()
        locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        locationRequest.interval = (10 * 1000).toLong()
        locationRequest.fastestInterval = 2000
        val builder = LocationSettingsRequest.Builder()
        builder.addLocationRequest(locationRequest)
        val locationSettingsRequest = builder.build()
        val result = LocationServices.getSettingsClient(this).checkLocationSettings(locationSettingsRequest)
        result.addOnCompleteListener { task ->
            try {
                val response = task.getResult(ApiException::class.java)
                if (response!!.locationSettingsStates.isLocationPresent){
                    getLastLocation()
                }
            } catch (exception: ApiException) {
                when (exception.statusCode) {
                    LocationSettingsStatusCodes.RESOLUTION_REQUIRED -> try {
                        val resolvable = exception as ResolvableApiException
                        resolvable.startResolutionForResult(this, REQUEST_CHECK_SETTINGS)
                    } catch (e: IntentSender.SendIntentException) {
                    } catch (e: ClassCastException) {
                    }
                    LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE -> { }
                }
            }
        }
    }
    private fun getLastLocation() {
        fusedLocationProviderClient.lastLocation
            .addOnCompleteListener(this) { task ->
                if (task.isSuccessful && task.result != null) {
                    val mLastLocation = task.result
                    setStartLocation(mLastLocation!!.latitude, mLastLocation.longitude, "")
                } else {
                    Toast.makeText(this, "No current location found", Toast.LENGTH_LONG).show()
                }
            }
    }
    private fun setStartLocation(lat: Double, lng: Double, addr: String){
        var address = "Current address"
        if (addr.isEmpty()){
            val gcd = Geocoder(this, Locale.getDefault())
            val addresses: List<Address>
            try {
                addresses = gcd.getFromLocation(lat, lng, 1)
                if (addresses.isNotEmpty()) {
                    address = addresses[0].getAddressLine(0)
                }
            } catch (e: IOException) {
                e.printStackTrace()
            }
        } else {
            address = addr
        }
        val icon = BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(this.resources, R.drawable.ic_pickup))
        startMarker = googleMap.addMarker(
            MarkerOptions()
                .position(LatLng(lat, lng))
                .title("Start Location")
                .snippet("Near $address")
                .icon(icon)
                .draggable(true)
        )
        val cameraPosition = CameraPosition.Builder()
            .target(LatLng(lat, lng))
            .zoom(17f)
            .build()
        googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
        fromLocationTxt.text = String.format("From: Near %s", address)
    }
    private fun setFinishLocation(lat: Double, lng: Double, addr: String){
        var address = "Destination address"
        if (addr.isEmpty()){
            val gcd = Geocoder(this, Locale.getDefault())
            val addresses: List<Address>
            try {
                addresses = gcd.getFromLocation(lat, lng, 1)
                if (addresses.isNotEmpty()) {
                    address = addresses[0].getAddressLine(0)
                }
            } catch (e: IOException) {
                e.printStackTrace()
            }
        } else {
            address = addr
        }
        val icon = BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(this.resources, R.drawable.drop_pin))
        finishMarker = googleMap.addMarker(
            MarkerOptions()
                .position(LatLng(lat, lng))
                .title("Finish Location")
                .snippet("Near $address")
                .icon(icon)
                .draggable(true)
        )
        val cameraPosition = CameraPosition.Builder()
            .target(LatLng(lat, lng))
            .zoom(17f)
            .build()
        googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
        toLocationTxt.text = String.format("To: Near %s", address)
    }
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == CURRENT_PLACE_AUTOCOMPLETE_REQUEST_CODE){
            when (resultCode) {
                AutocompleteActivity.RESULT_OK -> {
                    val place = Autocomplete.getPlaceFromIntent(data!!)
                    val latLng = place.latLng.toString()
                    val location = latLng.substring(latLng.indexOf("(") + 1, latLng.indexOf(")"))
                    val loc = location.split(",")
                    setStartLocation(loc[0].toDouble(), loc[1].toDouble(), place.name!!)
                }
                AutocompleteActivity.RESULT_ERROR -> {
                    val status = Autocomplete.getStatusFromIntent(intent)
                    Toast.makeText(this, status.statusMessage, Toast.LENGTH_SHORT).show()
                }
                AutocompleteActivity.RESULT_CANCELED -> Toast.makeText(this, getString(R.string.request_cancelled), Toast.LENGTH_SHORT).show()
            }
        } else if (requestCode == DESTINATION_PLACE_AUTOCOMPLETE_REQUEST_CODE){
            when (resultCode) {
                AutocompleteActivity.RESULT_OK -> {
                    val place = Autocomplete.getPlaceFromIntent(data!!)
                    val latLng = place.latLng.toString()
                    val location = latLng.substring(latLng.indexOf("(") + 1, latLng.indexOf(")"))
                    val loc = location.split(",")
                    setFinishLocation(loc[0].toDouble(), loc[1].toDouble(), place.name!!)
                }
                AutocompleteActivity.RESULT_ERROR -> {
                    val status = Autocomplete.getStatusFromIntent(intent)
                    Toast.makeText(this, status.statusMessage, Toast.LENGTH_SHORT).show()
                }
                AutocompleteActivity.RESULT_CANCELED -> Toast.makeText(this, getString(R.string.request_cancelled), Toast.LENGTH_SHORT).show()
            }
        } else if (requestCode == REQUEST_CHECK_SETTINGS){
            if (resultCode == Activity.RESULT_OK) {
                getCurrentLocation()
            }
        }
    }
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_settings -> true
            else -> super.onOptionsItemSelected(item)
        }
    }
}

Here are the screens:

Happy coding! You can download the code from the GitHub!. Check more guides on Dev

Top Best Android Programming Books 2019

How To Get and Display Current User Location on Android Google Map

How to code Android Launcher Screen (Splash Screen) for your app the right way

Cart

Your Cart is Empty

Back To Shop