EV Sum 2 uses Google Play Services Location API to access device location with reverse geocoding capabilities that convert coordinates to human-readable addresses using the Chilean Spanish locale.
Overview
The geolocation feature provides accurate device location using GPS or network providers, with automatic address lookup through Android’s Geocoder API.
The feature uses Google Play Services Fused Location Provider for optimal battery efficiency and accuracy.
Architecture
The location feature consists of:
LocationService (services/LocationService.kt:19) - High-level service with reverse geocoding
LocationRepository (data/repositories/LocationRepository.kt:11) - Low-level location access
DeviceLocation (domain/models/DeviceLocation.kt:3) - Location data model
Data models
DeviceLocation
data class DeviceLocation (
val latitude: Double ,
val longitude: Double
)
LocationResult
data class LocationResult (
val coords: DeviceLocation ,
val address: String ?
)
LocationService implementation
The service combines location retrieval with reverse geocoding:
class LocationService ( private val context: Context ) {
private val repo = LocationRepository (context)
fun hasPermission (): Boolean = repo. hasPermission ()
@RequiresPermission (anyOf = [
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
])
suspend fun getLocationWithAddress (): LocationResult {
return try {
val (lat, lng) = repo. getLatLng ()
val address = reverseGeocodeSafe (lat, lng)
LocationResult ( DeviceLocation (lat, lng), address)
} catch (e: SecurityException ) {
throw IllegalStateException ( "Location permission not granted." , e)
}
}
private suspend fun reverseGeocodeSafe (lat: Double , lon: Double ): String ? {
return try {
val geocoder = Geocoder (context, Locale. forLanguageTag ( "es-CL" ))
if (Build.VERSION.SDK_INT >= 33 ) {
suspendCancellableCoroutine { cont ->
geocoder. getFromLocation (lat, lon, 1 ) { results ->
if ( ! cont.isActive) return @getFromLocation
val line = results. firstOrNull ()?. getAddressLine ( 0 )
cont. resume (line)
}
}
} else {
@Suppress ( "DEPRECATION" )
geocoder. getFromLocation (lat, lon, 1 )
?. firstOrNull ()
?. getAddressLine ( 0 )
}
} catch (_: Exception ) {
null
}
}
}
LocationRepository implementation
The repository handles low-level location access:
class LocationRepository ( private val context: Context ) {
private val fused by lazy {
LocationServices. getFusedLocationProviderClient (context)
}
fun hasPermission (): Boolean {
val fine = ContextCompat. checkSelfPermission (
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
val coarse = ContextCompat. checkSelfPermission (
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
return fine || coarse
}
suspend fun getLatLng (): Pair < Double , Double > {
try {
// Try last known location first
val last = fused.lastLocation. await ()
if (last != null ) return last.latitude to last.longitude
// Request current location if no last location
val current = fused. getCurrentLocation (
Priority.PRIORITY_HIGH_ACCURACY,
null
). await ()
if (current != null ) return current.latitude to current.longitude
throw IllegalStateException (
"Location unavailable (GPS off or no signal)."
)
} catch (se: SecurityException ) {
throw IllegalStateException ( "Location permission not granted." , se)
} catch (e: Exception ) {
throw IllegalStateException ( "Error getting location: ${ e.message } " , e)
}
}
}
Usage example
Initialize service
Create the location service with context: val context = LocalContext.current
val locationService = remember { LocationService (context) }
Check permissions
Verify location permissions before accessing location: if ( ! locationService. hasPermission ()) {
// Request permissions
locationPermissionLauncher. launch (
arrayOf (
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
}
Get location
Retrieve location with address: scope. launch {
try {
val result = locationService. getLocationWithAddress ()
// Access coordinates
val lat = result.coords.latitude
val lng = result.coords.longitude
// Access address (may be null)
val address = result.address ?: "Address unavailable"
} catch (e: Exception ) {
// Handle error
}
}
Permissions
Add location permissions to your AndroidManifest.xml:
< uses-permission android:name = "android.permission.ACCESS_FINE_LOCATION" />
< uses-permission android:name = "android.permission.ACCESS_COARSE_LOCATION" />
Permission levels
Fine Location
Coarse Location
ACCESS_FINE_LOCATION provides GPS-level accuracy:
Accuracy: ~5-10 meters
Power consumption: Higher
Best for: Navigation, precise location needs
ACCESS_COARSE_LOCATION provides network-based location:
Accuracy: ~100-500 meters
Power consumption: Lower
Best for: General location, city-level accuracy
Runtime permission request
val locationPermissionLauncher = rememberLauncherForActivityResult (
contract = ActivityResultContracts. RequestMultiplePermissions ()
) { permissions ->
when {
permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true -> {
// Fine location granted
getLocation ()
}
permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true -> {
// Coarse location granted
getLocation ()
}
else -> {
// No location permission
showError ( "Location permission required" )
}
}
}
// Request permissions
locationPermissionLauncher. launch (
arrayOf (
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
Reverse geocoding
The service automatically converts coordinates to addresses using the Geocoder API:
private suspend fun reverseGeocodeSafe (lat: Double , lon: Double ): String ? {
return try {
val geocoder = Geocoder (context, Locale. forLanguageTag ( "es-CL" ))
if (Build.VERSION.SDK_INT >= 33 ) {
// Use callback-based API for Android 13+
suspendCancellableCoroutine { cont ->
geocoder. getFromLocation (lat, lon, 1 ) { results ->
if ( ! cont.isActive) return @getFromLocation
val line = results. firstOrNull ()?. getAddressLine ( 0 )
cont. resume (line)
}
}
} else {
// Use synchronous API for older versions
@Suppress ( "DEPRECATION" )
geocoder. getFromLocation (lat, lon, 1 )
?. firstOrNull ()
?. getAddressLine ( 0 )
}
} catch (_: Exception ) {
null // Return null if geocoding fails
}
}
Reverse geocoding uses Chilean Spanish locale (es-CL) for localized address formatting.
Geocoding behavior
Success : Returns formatted address (e.g., “Av. Libertador Bernardo O’Higgins 1234, Santiago, Chile”)
Failure : Returns null (graceful degradation)
Offline : May return null if Geocoder requires network
Location strategy
The repository uses a two-tier approach for optimal performance:
Last known location
First, try to get the last known location from cache: val last = fused.lastLocation. await ()
if (last != null ) return last.latitude to last.longitude
This is instant and battery-efficient.
Current location
If no cached location exists, request a fresh location: val current = fused. getCurrentLocation (
Priority.PRIORITY_HIGH_ACCURACY,
null
). await ()
This may take a few seconds but provides accurate results.
Using last known location first provides instant results in most cases, falling back to current location only when necessary.
Error handling
The service handles various error scenarios:
Error : SecurityException → “Location permission not granted.”Cause : User hasn’t granted location permissionsSolution : Request ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION
Error : IllegalStateException → “Location unavailable (GPS off or no signal).”Cause : GPS is disabled or device can’t acquire locationSolution : Prompt user to enable location services
Result : address is nullCause : Network unavailable or geocoding service errorSolution : Display coordinates only or show “Address unavailable”
Complete example
Here’s a complete implementation with error handling:
val locationService = remember { LocationService (context) }
var location by remember { mutableStateOf < DeviceLocation ?>( null ) }
var address by remember { mutableStateOf < String ?>( null ) }
var error by remember { mutableStateOf < String ?>( null ) }
var isLoading by remember { mutableStateOf ( false ) }
val locationPermissionLauncher = rememberLauncherForActivityResult (
contract = ActivityResultContracts. RequestMultiplePermissions ()
) { permissions ->
if (permissions.values. any { it }) {
// At least one permission granted
scope. launch {
isLoading = true
try {
val result = locationService. getLocationWithAddress ()
location = result.coords
address = result.address
error = null
} catch (e: Exception ) {
error = e.message ?: "Error getting location"
} finally {
isLoading = false
}
}
} else {
error = "Location permission required"
}
}
Button (
onClick = {
if (locationService. hasPermission ()) {
scope. launch {
isLoading = true
try {
val result = locationService. getLocationWithAddress ()
location = result.coords
address = result.address
error = null
} catch (e: Exception ) {
error = e.message
} finally {
isLoading = false
}
}
} else {
locationPermissionLauncher. launch (
arrayOf (
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
}
}
) {
Text ( if (isLoading) "Loading..." else "Get Location" )
}
// Display results
location?. let {
Text ( "Latitude: ${ it.latitude } " )
Text ( "Longitude: ${ it.longitude } " )
}
address?. let {
Text ( "Address: $it " )
}
error?. let {
Text (it, color = MaterialTheme.colorScheme.error)
}
Best practices
Check permissions Always verify permissions before requesting location to avoid SecurityException.
Handle null addresses Geocoding may fail - always handle null addresses gracefully.
Show loading state Location requests can take time - provide visual feedback to users.
Battery efficiency Use last known location when possible to conserve battery.
Location services require Google Play Services to be installed and enabled on the device.
Dependencies
Add Google Play Services location to your build.gradle:
dependencies {
implementation 'com.google.android.gms:play-services-location:21.0.1'
}
Testing considerations
Emulator : Use emulator extended controls to simulate GPS coordinates
Physical device : Test both indoors (may fail) and outdoors (GPS signal)
Permissions : Test both permission grant and denial scenarios
Network : Test geocoding with and without internet connection
Authentication User authentication for personalized features
Phrase Management Store location-tagged phrases