Muyiwa Femi-Ige Femi-Ige Muyiwa Oladele is a statistics major from the Federal University of Technology, Minna. He is an enthusiastic programmer versed in programming languages like Python and JavaScript.

How to build a geocaching app with Android’s Fused Location

7 min read 2184

How To Build A Geocaching App With Android’s Fused Location

According to the Oxford dictionary, geocaching refers to “an activity or pastime in which an item, or a container holding several items, is hidden at a particular location for GPS users to find using coordinates posted on the internet.”

For a geocaching application, we want the app to notify a user when they are within a specific radius of item A. Let’s say that the user, represented by a marker, has an item stored in a coordinate represented by another marker. In this case, the marker for the item is static, while the marker for the user is dynamic.

Using the Fused Location library in Android, we can build a geocaching application that provides a background service notification about the current user’s coordinates. The user will receive a notification if they are within a five-mile radius of the cache and will continue to be updated a distance calculation if they move closer to or further from the item.

At the end of the tutorial, our application will look like this:


Geocaching App

To jump ahead:


The reader is required to have the Android Studio code editor and Kotlin on their specific device.

Getting started

We will start by creating a Google MapFragment. To do so, create a new Android Studio project. Select Google Maps Activity as your template and fill in our app name and package name. Doing that takes a lot of processes off the board because now we only need to get an API key from the Google Console:

Google MapFragment Process

New Project In Android Studio

Google Maps Activity Project

Next, we will go to the Google developer’s console to get the API key.

Then, select Create Credentials and API key to create an API key:

Copy our newly created key, head to the AndroidManifest.xml file, and paste it into the metadata tag attribute value with the keyword API key:

Paste New Key Into Metadata Tag

Creating the functionalities

After following the steps above, we only have a custom Google Map created automatically by Android Studio. In this section, we want to use the fusedlcation API to get a continuous location update for the user, even after closing the application. We’ll achieve this via a background notification update.

To begin, head over to the module build.gradle file and add the dependencies below:

implementation ''
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"

Adding Dependencies To Build.Gradle File

Next, head back to the AndroidManifest.xml file and set the following permissions, right above the applications tag:

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

Creating abstractions and permission settings

We do not want to open an application and automatically have location access (well, applications don’t work like that). Instead, we want the client to get a notification asking for access to some system settings.

We’ll build an interface file that abstracts the location updates within the root folder and call it ClientInfo.kt. Within the interface, we’ll create a function with the parameter interval that specifies how often we want our location updated. The function will return a Flow of type Location from the coroutines library we added to the dependencies earlier.

We will also create a class to pass a message in case our GPS is turned off:

interface ClientInfo {
    fun getLocationUpdates(interval: Long): Flow<Location>

    class LocException(message: String): Exception()

Now, we need to show the implementation of the ClientInfo. So, within the same root folder, create a class file called DefaultClientInfo.kt, which will implement the interface (ClientInfo) we declared above. The class will then take two constructor parameters: Context and FusedLocationProviderClient.

Next, we will override the getLocationUpdates function, and using the callbackFlow instance, we’ll first check if the user has accepted the location permission. We’ll do this by creating a utility file in the same root folder called ExtendContext.kt to write an extension function that returns a Boolean.

This function will check if the COARSE and FINE_LOCATION permissions are granted:

fun Context.locationPermission(): Boolean{
    return  ContextCompat.checkSelfPermission(
    )== PackageManager.PERMISSION_GRANTED &&
            ) == PackageManager.PERMISSION_GRANTED

If the user has allowed permission, we want to check if they can fetch their location (if the location is enabled) using the SytemService LocationManager.

Now that we can fetch the user’s location, we need to create a request that will specify how often we want to fetch the user’s location and the accuracy of the data. Also, we will create a callback that will use the onLocationResult function whenever the FusedLocationProviderClient fetches a new location.

Finally, we will use the fusedlocation.requestLocationUpdates method to call the callback function, request, and a looper. Here is the implementation:

class DefaultClientInfo(
    private val context:Context,
    private val fusedlocation: FusedLocationProviderClient

    override fun getLocationUpdates(interval: Long): Flow<Location> {
        return callbackFlow {
                throw ClientInfo.LocException("Missing Permission")

            val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
            val hasGPS = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
            val hasNetwork = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
            if(!hasGPS && hasNetwork){
                throw  ClientInfo.LocException("GPS is unavailable")

            val locationRequest = LocationRequest.create().apply {
                fastestInterval = interval
                priority = Priority.PRIORITY_HIGH_ACCURACY
            val locationCallback = object : LocationCallback(){
                override fun onLocationResult(result: LocationResult) {
                    result.locations.lastOrNull()?.let{ location ->
                        launch { send(location) }


            awaitClose {

Creating a foreground service

To create a foreground service, we will create another class file in our root project called locservices.kt and make it inherit from the service class. Using a coroutine, we will create a serviceScope that is bound to the lifetime of the service, call the ClientInfo abstraction that we created earlier, and a class that stores the coordinate information of our cache.

class LocServices: Service(){

    private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private lateinit var clientInfo: ClientInfo


Next, we will create an onBind function that will return null because we are not binding our service to anything. Then, we will use the onCreate function to call the DefaultClientInfo class, in which we will provide applicationContext and
LocationServices.getFusedLocationProviderClient(applicationContext) as the parameters.

class LocServices: Service(){

    // do something

    override fun onBind(p0: Intent?): IBinder? {
        return null

    override fun onCreate() {
        clientInfo = DefaultClientInfo(

Now, we will create a companion object and, within it, create a constant value START, which we send to the service when we want to start our tracking. Then we will call the onStartCommand() function for services and provide the constant we created earlier as an intent that we linked to a start() function:

class LocServices: Service(){

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
            START -> start()
        return super.onStartCommand(intent, flags, startId)

    private fun start(){

    companion object{
        const val START = "Start"

The start function will handle the notification to alert the user that their location is actively being monitored. That means the information we want to provide the user is the distance (in meters) between them and the cache. To do that, we will use the Haversine formula, which computes the distance between two points on a sphere using their coordinates.

Thus, using our callbackflow, we will call the clientInfo.getLocationUpdates(interval) method, and using the onEach method provided by coroutines, we will be able to get the updated latitude and longitude.

As we said earlier, we want the user to know the distance between them and the cache, but there is a catch. We do not want the user to get a consistent flurry of notifications telling them the distance between them and the cache.

So, we will create a conditional statement that checks if the user is within a thousand-meter radius of the cache. If true, the user will get an ongoing notification informing them if they are getting further or closer to the cache. Once they get within a 50-meter radius, they are notified with a different message, and the service stops:

class LocServices: Service(){

    private fun start(){
        val notif = NotificationCompat.Builder(this, "location")
            .setContentText("runnning in the background")
        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

            .catch { e -> e.printStackTrace() }
            .onEach { location ->
                val lat1 = location.latitude
                val long1 = location.longitude
                val radius = 6371 //in km
                val lat2 = secrets.d
                val long2 = secrets.d1
                val dlat = Math.toRadians(lat2 - lat1)
                val dlong = Math.toRadians(long2 - long1)
                val a = sin(dlat / 2) * sin(dlong / 2) + cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) * sin(dlong / 2) * sin(dlong / 2)
                val c = 2 * asin(sqrt(a))
                val valuresult = radius * c
                val km = valuresult / 1
                val meter = km * 1000
                val truemeter = String.format("%.2f", meter)
                if (meter > 100 && meter <= 1000){
                    val updatednotif = notif
                        .setContentText("You are $truemeter meters away")
                if (meter < 100){
                    val getendnotice = notif
                        .setContentText("You are $truemeter meters away, continue with your search")

Finally, we will create an onDestroy function that cancels the service when we close the application or clear our system cache. Here is the implementation of the code below:

class LocServices: Service(){
    override fun onDestroy() {

Now that we have the foreground service ready, we will return to the AndroidManifest.xml file and the tag right above the metadata tag:

<service android:name=".fusedLocation.LocServices"
    android:foregroundServiceType = "location"/>


If we want to create a notification for our user’s distance to the cache, we need to create a channel to send notifications. Let’s first create a class called LocationApp.kt that we will make an Application().

In the onCreate function, we’ll create a notification channel from the Android oreo OS upwards. This is how the code looks:

class LocationApp: Application() {

    override fun onCreate() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
            val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

Finally, we will add the attribute to the application tag of the AndroidManifest.xml file below:



When we created a Google Maps Activity, we got a MapsActivity.kt file rather than the regular MainActivity.kt. This file handles the creation of a map with a marker. We need to make a few changes to that. So, let’s create three private lateinit variables: LocationCallback, LocationRequest and FusedLocationProviderClient.

Next, we will create three functions; launchintent, getupdatedlocation, and startupdate. We will call them in the onMapReady callback function.

The launchintent function handles the location permission request, and the getupdatedlocation function takes the LocationRequest and LocationCallback. The getupdatedlocation function will also handle calling the start function using its intent.

Finally, within the startupdate function, we will use the fusedlocation.requestLocationUpdates method to call the callback function, request, and a looper (set to null).

Here is how the code looks:

class MapsActivity : AppCompatActivity(), OnMapReadyCallback{

    companion object{
        private var firsttime = true

    private lateinit var mMap: GoogleMap
    private lateinit var binding: ActivityMapsBinding
    private lateinit var locationCallback: LocationCallback
    private lateinit var locationRequest: LocationRequest
    private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
    private var mMarker: Marker? = null
    private var secrets = Secretlocation()

    override fun onCreate(savedInstanceState: Bundle?) {

        binding = ActivityMapsBinding.inflate(layoutInflater)
        fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)

        // Obtain the SupportMapFragment and get notified when the map is ready to be used.
        val mapFragment = supportFragmentManager
            .findFragmentById( as SupportMapFragment

    private fun launchintent() {

    private fun getupdatedlocation(){
        locationRequest = LocationRequest.create().apply {
            interval = 10000
            fastestInterval = 5000
            priority = Priority.PRIORITY_HIGH_ACCURACY

        locationCallback = object : LocationCallback(){
            override fun onLocationResult(result: LocationResult) {
                if (result.locations.isNotEmpty()){
                    val location = result.lastLocation
                    if (location != null){
                        val lat1 = location.latitude
                        val long1 = location.longitude
                        val d = secrets.d
                        val d1 = secrets.d1
                        val latlong = LatLng(lat1, long1)
                        val stuff = LatLng(d, d1)

                        val stuffoption= MarkerOptions().position(stuff).title("$stuff").icon(
                        mMarker = mMap.addMarker(stuffoption)
                        val markerOptions = MarkerOptions().position(latlong).title("$latlong")
                        mMarker = mMap.addMarker(markerOptions)
                        if (firsttime){
                            mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latlong, 17f ))
                            firsttime = false
        Intent(applicationContext, {
            action = LocServices.START

    private fun startupdate(){

    override fun onMapReady(googleMap: GoogleMap) {
        mMap = googleMap
        mMap.uiSettings.isZoomControlsEnabled = true

When we run our application, we should have the result below:

Final App Demo


In this tutorial, we created a map using Android’s Fused Location library, which continuously updates the location of a user on a map. We also created a foreground service determining the distance between the user and a specific item. Finally, we created a notification for whenever our user gets closer to the cache.

Thanks for reading, and happy coding!

LogRocket: Instantly recreate issues in your Android apps.

LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your Android apps — try LogRocket for free.

Muyiwa Femi-Ige Femi-Ige Muyiwa Oladele is a statistics major from the Federal University of Technology, Minna. He is an enthusiastic programmer versed in programming languages like Python and JavaScript.

Leave a Reply