In response to the Oxford dictionary, geocaching refers to “an exercise or pastime during which an merchandise, or a container holding a number of objects, is hidden at a selected location for GPS customers to search out utilizing coordinates posted on the web.”
For a geocaching software, we would like the app to inform a consumer when they’re inside a particular radius of merchandise A. Let’s say that the consumer, represented by a marker, has an merchandise saved in a coordinate represented by one other marker. On this case, the marker for the merchandise is static, whereas the marker for the consumer is dynamic.
Utilizing the Fused Location library in Android, we will construct a geocaching software that gives a background service notification concerning the present consumer’s coordinates. The consumer will obtain a notification if they’re inside a five-mile radius of the cache and can proceed to be up to date a distance calculation in the event that they transfer nearer to or farther from the merchandise.
On the finish of the tutorial, our software will appear like this:
To leap forward:
Stipulations
The reader is required to have the Android Studio code editor and Kotlin on their particular machine.
Getting began
We are going to begin by making a Google MapFragment
. To take action, create a brand new Android Studio venture. Choose Google Maps Exercise as your template and fill in our app identify and bundle identify. Doing that takes a number of processes off the board as a result of now we solely have to get an API key from the Google Console:
Subsequent, we are going to go to the Google developer’s console to get the API key.
Then, choose 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 worth
with the key phrase API key:
Creating the functionalities
After following the steps above, we solely have a customized Google Map created robotically by Android Studio. On this part, we need to use the fusedlcation
API to get a steady location replace for the consumer, even after closing the applying. We’ll obtain this by way of a background notification replace.
To start, head over to the module construct.gradle
file and add the dependencies under:
implementation 'com.google.android.gms:play-services-location:20.0.0' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
Subsequent, head again to the AndroidManifest.xml
file and set the next permissions, proper above the functions tag:
<uses-permission android:identify="android.permission.INTERNET" /> <uses-permission android:identify="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:identify="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:identify="android.permission.FOREGROUND_SERVICE" />
Creating abstractions and permission settings
We don’t need to open an software and robotically have location entry (effectively, functions don’t work like that). As an alternative, we would like the consumer to get a notification asking for entry to some system settings.
We’ll construct an interface file that abstracts the placement updates throughout the root folder and name it ClientInfo.kt
. Inside the interface, we’ll create a operate with the parameter interval that specifies how typically we would like our location up to date. The operate will return a Move
of sort Location
from the coroutines library we added to the dependencies earlier.
We will even create a category to move a message in case our GPS is turned off:
interface ClientInfo { enjoyable getLocationUpdates(interval: Lengthy): Move<Location> class LocException(message: String): Exception() }
Now, we have to present the implementation of the ClientInfo
. So, throughout the identical root folder, create a category file known as DefaultClientInfo.kt
, which can implement the interface (ClientInfo
) we declared above. The category will then take two constructor parameters: Context
and FusedLocationProviderClient
.
Subsequent, we are going to override the getLocationUpdates
operate, and utilizing the callbackFlow
occasion, we’ll first examine if the consumer has accepted the placement permission. We’ll do that by making a utility file in the identical root folder known as ExtendContext.kt
to write down an extension operate that returns a Boolean.
This operate will examine if the COARSE
and FINE_LOCATION
permissions are granted:
enjoyable Context.locationPermission(): Boolean{ return ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_COARSE_LOCATION )== PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED }
If the consumer has allowed permission, we need to examine if they’ll fetch their location (if the placement is enabled) utilizing the SytemService LocationManager
.
Now that we will fetch the consumer’s location, we have to create a request that may specify how typically we need to fetch the consumer’s location and the accuracy of the info. Additionally, we are going to create a callback that may use the onLocationResult
operate each time the FusedLocationProviderClient fetches a brand new location.
Lastly, we are going to use the fusedlocation.requestLocationUpdates
technique to name the callback operate, request, and a looper. Right here is the implementation:
class DefaultClientInfo( non-public val context:Context, non-public val fusedlocation: FusedLocationProviderClient ):ClientInfo{ @SuppressLint("MissingPermission") override enjoyable getLocationUpdates(interval: Lengthy): Move<Location> { return callbackFlow { if(!context.locationPermission()){ throw ClientInfo.LocException("Lacking 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 { setInterval(interval) fastestInterval = interval precedence = Precedence.PRIORITY_HIGH_ACCURACY } val locationCallback = object : LocationCallback(){ override enjoyable onLocationResult(outcome: LocationResult) { tremendous.onLocationResult(outcome) outcome.areas.lastOrNull()?.let{ location -> launch { ship(location) } } } } fusedlocation.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() ) awaitClose { fusedlocation.removeLocationUpdates(locationCallback) } } } }
Making a foreground service
To create a foreground service, we are going to create one other class file in our root venture known as locservices.kt
and make it inherit from the service class. Utilizing a coroutine, we are going to create a serviceScope
that’s sure to the lifetime of the service, name the ClientInfo
abstraction that we created earlier, and a category that shops the coordinate info of our cache.
class LocServices: Service(){ non-public val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) non-public lateinit var clientInfo: ClientInfo }
Subsequent, we are going to create an onBind
operate that may return null
as a result of we aren’t binding our service to something. Then, we are going to use the onCreate
operate to name the DefaultClientInfo
class, during which we are going to present applicationContext
andLocationServices.getFusedLocationProviderClient(applicationContext)
because the parameters.
class LocServices: Service(){ // do one thing override enjoyable onBind(p0: Intent?): IBinder? { return null } override enjoyable onCreate() { tremendous.onCreate() clientInfo = DefaultClientInfo( applicationContext, LocationServices.getFusedLocationProviderClient(applicationContext) ) } }
Now, we are going to create a companion object
and, inside it, create a relentless worth START
, which we ship to the service after we need to begin our monitoring. Then we are going to name the onStartCommand()
operate for companies and supply the fixed we created earlier as an intent
that we linked to a begin()
operate:
class LocServices: Service(){ override enjoyable onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when(intent?.motion){ START -> begin() } return tremendous.onStartCommand(intent, flags, startId) } @SuppressLint("NewApi") non-public enjoyable begin(){ } companion object{ const val START = "Begin" } }
The begin
operate will deal with the notification to alert the consumer that their location is actively being monitored. Meaning the knowledge we need to present the consumer is the space (in meters) between them and the cache. To try this, we are going to use the Haversine formulation, which computes the space between two factors on a sphere utilizing their coordinates.
Thus, utilizing our callbackflow
, we are going to name the clientInfo.getLocationUpdates(interval)
technique, and utilizing the onEach
technique offered by coroutines
, we will get the up to date latitude and longitude.
As we stated earlier, we would like the consumer to know the space between them and the cache, however there’s a catch. We are not looking for the consumer to get a constant flurry of notifications telling them the space between them and the cache.
So, we are going to create a conditional assertion that checks if the consumer is inside a thousand-meter radius of the cache. If true, the consumer will get an ongoing notification informing them if they’re getting additional or nearer to the cache. As soon as they get inside a 50-meter radius, they’re notified with a unique message, and the service stops:
class LocServices: Service(){ @SuppressLint("NewApi") non-public enjoyable begin(){ val notif = NotificationCompat.Builder(this, "location") .setContentTitle("Geocaching") .setContentText("runnning within the background") .setSmallIcon(R.drawable.ic_launcher_background) .setOngoing(true) val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager clientInfo .getLocationUpdates(1000L) .catch { e -> e.printStackTrace() } .onEach { location -> val lat1 = location.latitude val long1 = location.longitude val radius = 6371 //in km val lat2 = secrets and techniques.d val long2 = secrets and techniques.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 might be $truemeter meters away") notificationManager.notify(1, updatednotif.construct()) } if (meter < 100){ val getendnotice = notif .setContentText("You might be $truemeter meters away, proceed together with your search") .setOngoing(false) notificationManager.notify(1, getendnotice.construct()) stopForeground(STOP_FOREGROUND_DETACH) stopSelf() } } .launchIn(serviceScope) startForeground(1, notif.construct()) } }
Lastly, we are going to create an onDestroy
operate that cancels the service after we shut the applying or clear our system cache. Right here is the implementation of the code under:
class LocServices: Service(){ override enjoyable onDestroy() { tremendous.onDestroy() serviceScope.cancel() } }
Now that we’ve the foreground service prepared, we are going to return to the AndroidManifest.xml
file and the tag proper above the metadata tag:
<service android:identify=".fusedLocation.LocServices" android:foregroundServiceType = "location"/>
NotificationChannel
If we need to create a notification for our consumer’s distance to the cache, we have to create a channel to ship notifications. Let’s first create a category known as LocationApp.kt
that we are going to make an Software()
.
Within the onCreate
operate, we’ll create a notification channel from the Android oreo OS upwards. That is how the code seems to be:
class LocationApp: Software() { override enjoyable onCreate() { tremendous.onCreate() if (Construct.VERSION.SDK_INT >= Construct.VERSION_CODES.O) { val channel = NotificationChannel( "location", "Location", NotificationManager.IMPORTANCE_LOW ) val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } } }
Lastly, we are going to add the attribute to the applying tag of the AndroidManifest.xml
file under:
android:identify=".fusedLocation.LocationApp"
MapsActivity.kt
After we created a Google Maps Exercise, we bought a MapsActivity.kt
file quite than the common MainActivity.kt
. This file handles the creation of a map with a marker. We have to make a couple of modifications to that. So, let’s create three non-public lateinit
variables: LocationCallback
, LocationRequest
and FusedLocationProviderClient
.
Subsequent, we are going to create three features; launchintent
, getupdatedlocation
, and startupdate
. We are going to name them within the onMapReady
callback operate.
The launchintent
operate handles the placement permission request, and the getupdatedlocation
operate takes the LocationRequest
and LocationCallback
. The getupdatedlocation
operate will even deal with calling the beginning operate utilizing its intent.
Lastly, throughout the startupdate
operate, we are going to use the fusedlocation.requestLocationUpdates
technique to name the callback operate, request, and a looper (set to null).
Extra nice articles from LogRocket:
Right here is how the code seems to be:
class MapsActivity : AppCompatActivity(), OnMapReadyCallback{ companion object{ non-public var firsttime = true } non-public lateinit var mMap: GoogleMap non-public lateinit var binding: ActivityMapsBinding non-public lateinit var locationCallback: LocationCallback non-public lateinit var locationRequest: LocationRequest non-public lateinit var fusedLocationProviderClient: FusedLocationProviderClient non-public var mMarker: Marker? = null non-public var secrets and techniques = Secretlocation() override enjoyable onCreate(savedInstanceState: Bundle?) { tremendous.onCreate(savedInstanceState) binding = ActivityMapsBinding.inflate(layoutInflater) setContentView(binding.root) fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this) // Receive the SupportMapFragment and get notified when the map is prepared for use. val mapFragment = supportFragmentManager .findFragmentById(R.id.map) as SupportMapFragment mapFragment.getMapAsync(this) } non-public enjoyable launchintent() { ActivityCompat.requestPermissions( this, arrayOf( Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION ), 0 ) } non-public enjoyable getupdatedlocation(){ locationRequest = LocationRequest.create().apply { interval = 10000 fastestInterval = 5000 precedence = Precedence.PRIORITY_HIGH_ACCURACY } locationCallback = object : LocationCallback(){ override enjoyable onLocationResult(outcome: LocationResult) { if (outcome.areas.isNotEmpty()){ val location = outcome.lastLocation if (location != null){ mMarker?.take away() val lat1 = location.latitude val long1 = location.longitude val d = secrets and techniques.d val d1 = secrets and techniques.d1 val latlong = LatLng(lat1, long1) val stuff = LatLng(d, d1) val stuffoption= MarkerOptions().place(stuff).title("$stuff").icon( BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE)) mMarker = mMap.addMarker(stuffoption) val markerOptions = MarkerOptions().place(latlong).title("$latlong") mMarker = mMap.addMarker(markerOptions) if (firsttime){ mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latlong, 17f )) firsttime = false } } } } } Intent(applicationContext, LocServices::class.java).apply { motion = LocServices.START startService(this) } } @SuppressLint("MissingPermission") non-public enjoyable startupdate(){ fusedLocationProviderClient.requestLocationUpdates( locationRequest, locationCallback, null ) } override enjoyable onMapReady(googleMap: GoogleMap) { mMap = googleMap launchintent() getupdatedlocation() startupdate() mMap.uiSettings.isZoomControlsEnabled = true } }
After we run our software, we should always have the outcome under:
Conclusion
On this tutorial, we created a map utilizing Android’s Fused Location library, which repeatedly updates the placement of a consumer on a map. We additionally created a foreground service figuring out the space between the consumer and a particular merchandise. Lastly, we created a notification for each time our consumer will get nearer to the cache.
Thanks for studying, and glad coding!
LogRocket: Immediately recreate points in your Android apps.
LogRocket is an Android monitoring resolution that helps you reproduce points immediately, prioritize bugs, and perceive efficiency in your Android apps.
LogRocket additionally helps you improve conversion charges and product utilization by displaying you precisely how customers are interacting together with your app. LogRocket’s product analytics options floor the the explanation why customers do not full a selected circulate or do not undertake a brand new function.
Begin proactively monitoring your Android apps — strive LogRocket without cost.