Location in Android apps for Android & Kotlin developers
Location data is one of the most powerful and sensitive features we can access in Android. When handled correctly, it enables ride-hailing, delivery tracking, weather personalisation, and geofencing. When handled poorly, it drains batteries, violates user privacy, and gets your app removed from the Play Store. This guide walks through the practical patterns our team uses to implement location responsibly in production Android apps.
Understanding the Fused Location Provider
Google Play Services provides the Fused Location Provider API, which abstracts away the complexity of choosing between GPS, network, and passive location sources. Instead of managing multiple location providers directly, we request location through a single, intelligent interface that balances accuracy and battery consumption.
The Fused Location Provider decides which sensors to use based on our location request priority and the device state. High-priority requests use GPS; low-priority requests might use WiFi and cellular triangulation. This is far smarter than manually managing LocationManager ourselves.
Here's how we set it up in a production app:
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import android.os.Looper
class LocationManager(context: Context) {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
private val locationRequest = LocationRequest.Builder(
priority = Priority.PRIORITY_HIGH_ACCURACY,
intervalMillis = 10000 // 10 seconds
).apply {
setMinUpdateIntervalMillis(5000)
setMaxUpdateAgeMillis(60000)
}.build()
fun startLocationUpdates(callback: (latitude: Double, longitude: Double) -> Unit) {
val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.locations.lastOrNull()?.let { location ->
callback(location.latitude, location.longitude)
}
}
}
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
}
}The key insight: let Google Play Services handle the decision logic. We specify our priority level and desired update frequency, then trust the system to choose sensors efficiently. This pattern scales across delivery apps processing thousands of driver locations simultaneously.
Coarse vs fine location: Choose the right granularity
Android distinguishes between two location permissions: ACCESS_FINE_LOCATION (GPS-level accuracy, typically 5–10 meters) and ACCESS_COARSE_LOCATION (network-level accuracy, typically 1–5 kilometers). Requesting the wrong one wastes battery and violates the principle of least privilege.
Coarse location is sufficient for most use cases: recommending nearby restaurants, filtering events by city, or showing regional weather. It consumes far less battery because it relies on WiFi and cellular data that the device is already checking. Users are also more likely to grant coarse permission because they perceive lower privacy risk.
Fine location is necessary only when precision matters: ride-hailing pickup, real-time turn-by-turn navigation, or geofencing within 100 meters. In our experience, fewer than 20% of location features genuinely need fine location.
Define your LocationRequest priority accordingly:
Priority.PRIORITY_HIGH_ACCURACY– Uses GPS; best for navigation and ride-hailing. Drain risk: high. Latency: 1–10 seconds.Priority.PRIORITY_BALANCED_POWER_ACCURACY– Balances GPS and network; good for checking-in or geofencing. Drain risk: medium.Priority.PRIORITY_LOW_POWER– Mostly network; acceptable for coarse features. Drain risk: low.Priority.PRIORITY_PASSIVE– Uses only location updates already being requested by other apps. No drain from us, but updates are infrequent and stale.
In a real app, we've found that filtering by distance server-side (requesting low power, then filtering clients > 500m away) works better than requesting high accuracy for hundreds of thousands of users simultaneously. The server becomes your rate limiter, not the device battery.
Background location needs strong justification
Background location—requesting location updates when the app is backgrounded or the screen is off—is heavily restricted on modern Android. Since Android 10, background location requires a separate permission (ACCESS_BACKGROUND_LOCATION) and the Google Play Store requires a strong, defensible reason.
Our rule of thumb: if you're not actively driving, delivering, or tracking for the user, don't request background location. The Play Store reviewers will reject it, and we've seen apps get suspended for misuse.
Legitimate use cases include:
- Ride-hailing and delivery apps tracking driver location during active jobs.
- Fitness apps recording a user's run or bike ride in the background.
- Geofencing apps alerting when the user crosses a boundary they explicitly set.
Poor use cases that we've seen rejected:
- Requesting background location to "improve ads targeting" without user knowledge.
- Continuous background tracking without a clear, real-time user benefit.
- Requesting background permission on first launch without explanation.
When background location is necessary, implement it with WorkManager for one-time or periodic tasks, or use Geofencing for event-driven triggers:
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit
class LocationTrackingWorker(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result = withContext(Dispatchers.Default) {
try {
val location = fetchCurrentLocation() // From FusedLocationProviderClient
uploadToServer(location)
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
// In your Activity or Service:
val locationTrackingWork = PeriodicWorkRequestBuilder(
15, TimeUnit.MINUTES
).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"location_tracking",
ExistingPeriodicWorkPolicy.KEEP,
locationTrackingWork
)This pattern respects battery and OS scheduling. The system batches work, defers it during low-battery mode, and respects Doze. And critically, it's transparent to users because WorkManager integrates with system battery settings.
Stop updates when not needed
The single biggest drain on battery in production location apps is forgetting to remove location listeners. When your Activity is backgrounded or destroyed, location callbacks keep firing. Over hours, this destroys battery and can trigger low-power mode complaints from users.
Always remove location requests in onPause() or onDestroy():
class LocationActivity : AppCompatActivity() {
private val fusedLocationClient by lazy {
LocationServices.getFusedLocationProviderClient(this)
}
private var locationCallback: LocationCallback? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
updateUI(locationResult.lastLocation)
}
}
startLocationUpdates()
}
private fun startLocationUpdates() {
val request = LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY,
10000
).build()
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
fusedLocationClient.requestLocationUpdates(
request,
locationCallback!!,
Looper.getMainLooper()
)
}
}
override fun onPause() {
super.onPause()
locationCallback?.let {
fusedLocationClient.removeLocationUpdates(it)
}
}
override fun onDestroy() {
super.onDestroy()
locationCallback = null
}
}The pattern is simple but critical: request in onCreate() or when the feature becomes active, remove in onPause() or onDestroy(). No exceptions. Use lint rules or static analysis to catch forgotten removals.
For long-lived location streams (like ride-hailing), use a foreground Service with a notification. The notification makes the drain visible to users and forces you to be honest about what you're doing:
class LocationTrackingService : Service() {
private val fusedLocationClient by lazy {
LocationServices.getFusedLocationProviderClient(this)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, "location_channel")
.setContentTitle("Tracking your location")
.setSmallIcon(R.drawable.ic_location)
.build()
startForeground(1, notification)
val request = LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY,
5000
).build()
fusedLocationClient.requestLocationUpdates(request, locationCallback, Looper.getMainLooper())
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
fusedLocationClient.removeLocationUpdates(locationCallback)
}
override fun onBind(intent: Intent?) = null
}Foreground Services are more visible and require explicit permission to start (on Android 12+), which keeps us honest about background location use.
Common mistakes
- Requesting fine location when coarse suffices. This wastes battery and fails Play Store review. Audit your features; most don't need GPS precision.
- Not removing location callbacks. We've debugged battery issues in production that traced back to a single forgotten removeLocationUpdates() call. Always pair request/remove in lifecycle methods.
- Updating too frequently. Requesting updates every second is unnecessary and drains battery. Most apps can tolerate 10–30 second intervals. Use
setMinUpdateIntervalMillis()to batch updates. - Ignoring permissions at runtime. Location requires runtime permissions on Android 6+. Always check with
checkSelfPermission()before requesting, and handle denials gracefully. - Backgrounding location without a foreground Service. Android 8+ will kill background Services unless you promote them to foreground. Use WorkManager or a foreground Service with a user-facing notification.
- Not testing with battery profilers. Android Studio's Profiler and Battery Historian reveal location drain. Profile before shipping; it's the only reliable way to know if your implementation is efficient.
Key takeaways
- Use the Fused Location Provider API; it abstracts provider selection and balances accuracy with battery.
- Choose coarse location unless precision is genuinely needed; most features don't need GPS.
- Background location requires strong user justification and Play Store approval; prefer WorkManager or Geofencing for scheduled or event-driven location tasks.
- Always remove location callbacks when the activity pauses or is destroyed; forgotten removals are the top cause of location-related battery drain in production apps.
- Set appropriate update intervals and priorities; batching updates and using low power mode for non-critical features makes a measurable difference.
- Use foreground Services for long-lived location streams; the user-facing notification keeps the feature honest and visible.
- Profile location drain with Android Profiler before shipping; perception of battery drain often causes more churn than actual location inaccuracy.






Comments
Loading comments…