Maps, Location & Device Features

Location Permissions with permission_handler

15 min Lesson 5 of 12

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.

Note: Location is a sensitive permission. On iOS, you must also declare usage descriptions in 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:
    NSLocationWhenInUseUsageDescription with 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();
    }
  }
}
Tip: On Android 12+, if the user denies a permission twice, 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 to ACCESS_FINE_LOCATION (precise)
  • Permission.locationWhenInUse — location only while app is in foreground
  • Permission.locationAlways — background location; requires a second request after locationWhenInUse is granted; most app stores scrutinise this heavily
Warning: Never request 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.