Location Permissions with permission_handler
Location Permissions with permission_handler
Mobile applications that access device location must request explicit permission from the user at runtime. Android and iOS both implement a runtime permissions model, meaning the user sees a system dialog and decides whether to grant or deny access — and that decision can be changed at any time in the device's Settings app. The permission_handler package from Flutter Community provides a unified Dart API to check and request permissions across both platforms without writing platform-specific code.
Info.plist. On Android, you must declare the relevant permissions in AndroidManifest.xml. The Dart code alone is insufficient — the native configuration must be present or the OS will silently ignore your request.Adding the Dependency
Add permission_handler to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
permission_handler: ^11.3.1
Then run flutter pub get. After that, complete the platform setup:
- Android — add to
AndroidManifest.xml(inside<manifest>, before<application>):
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> - iOS — add to
ios/Runner/Info.plist:
NSLocationWhenInUseUsageDescriptionwith a human-readable explanation string
Understanding Permission States
The package models every possible outcome as a value of the PermissionStatus enum. Knowing all states is essential for writing correct, user-friendly permission logic:
- granted — the user approved the permission; proceed normally.
- denied — the user tapped "Don't Allow" but can be asked again (Android) or has simply not been asked yet on the current install.
- permanentlyDenied — the user chose "Don't ask again" (Android) or has denied the iOS prompt twice. You cannot show the system dialog again; you must redirect to app settings.
- restricted — iOS only; parental controls or an MDM profile prevents granting this permission.
- limited — iOS 14+ only; the user granted access to a subset of their photos/location (not applicable to basic location).
- provisional — iOS notifications only; not relevant for location.
Checking and Requesting Permissions
Use Permission.location.status to check the current status without prompting, and Permission.location.request() to show the system dialog. Always check before requesting to avoid redundant dialogs:
import 'package:permission_handler/permission_handler.dart';
Future<void> handleLocationPermission() async {
// 1. Check current status (no dialog shown)
PermissionStatus status = await Permission.location.status;
if (status.isGranted) {
// Already granted — proceed directly
_startLocationTracking();
return;
}
if (status.isPermanentlyDenied) {
// Cannot show system dialog; guide user to Settings
_showSettingsDialog();
return;
}
// 2. Request permission (system dialog shown once)
status = await Permission.location.request();
if (status.isGranted) {
_startLocationTracking();
} else if (status.isPermanentlyDenied) {
_showSettingsDialog();
} else {
// Denied but can ask again later
_showPermissionRationale();
}
}
void _startLocationTracking() {
// TODO: use geolocator or google_maps_flutter
print('Location permission granted — starting tracking');
}
void _showPermissionRationale() {
print('Permission denied. Please grant location access to use this feature.');
}
void _showSettingsDialog() {
// Opens the app-specific Settings page on the device
openAppSettings();
}
Opening App Settings with openAppSettings()
When a permission is permanently denied, the only path forward is to send the user to the OS Settings screen so they can manually toggle the permission. The permission_handler package ships the openAppSettings() top-level function for exactly this purpose. It returns a Future<bool> indicating whether the Settings screen was successfully opened.
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
Future<void> requestLocationWithFallback(BuildContext context) async {
final status = await Permission.location.request();
if (status.isGranted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Location access granted!')),
);
return;
}
if (status.isPermanentlyDenied) {
final opened = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Location Required'),
content: const Text(
'Location permission was permanently denied. '
'Please enable it in Settings to continue.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Open Settings'),
),
],
),
) ?? false;
if (opened) {
await openAppSettings();
}
}
}
shouldShowRequestPermissionRationale returns false and the OS silently drops further requests. The isPermanentlyDenied check handles this case automatically — always branch on it before calling request().Precise vs Approximate Location (Android 12+)
Android 12 introduced a two-tier location model. Use Permission.locationWhenInUse for foreground access and Permission.locationAlways for background. On Android 12+, the user can also downgrade precise location to approximate. The permission_handler package exposes both granularities:
Permission.location— resolves toACCESS_FINE_LOCATION(precise)Permission.locationWhenInUse— location only while app is in foregroundPermission.locationAlways— background location; requires a second request afterlocationWhenInUseis granted; most app stores scrutinise this heavily
Permission.locationAlways without first obtaining locationWhenInUse. On iOS 13+ the OS enforces this order strictly. Requesting background location without a valid foreground grant causes a silent denial.Summary
Managing location permissions correctly requires four key steps: declare permissions in native manifests, check the current status before requesting, handle all PermissionStatus values including permanentlyDenied, and use openAppSettings() as the last-resort path. The permission_handler package makes all of this achievable in pure Dart with a clean, expressive API that works identically on Android and iOS.