Skip to content
FlutterLearn

Intermediate · Lesson 9 · 12 min read

Device Features and Permissions

Use the camera, location, files and sharing — and handle the permission flows that decide whether any of it works.

Updated August 1, 2026

What you will learn

  • Request permissions and handle every possible outcome
  • Capture and pick images, and manage the resulting files
  • Read location with the right accuracy for the job
  • Configure the native manifests each feature requires

Permissions are where Flutter's write-once promise gets thinnest. The Dart API is uniform, but Android and iOS have genuinely different rules about when you can ask, what happens on denial, and what a second refusal means.

The permission states

lib/platform/permissions.dart
import 'package:permission_handler/permission_handler.dart';

enum PermissionOutcome { granted, denied, permanentlyDenied, restricted }

Future<PermissionOutcome> requestCamera() async {
  final status = await Permission.camera.request();

  return switch (status) {
    PermissionStatus.granted || PermissionStatus.limited =>
      PermissionOutcome.granted,
    PermissionStatus.permanentlyDenied =>
      PermissionOutcome.permanentlyDenied,
    PermissionStatus.restricted => PermissionOutcome.restricted,
    _ => PermissionOutcome.denied,
  };
}
StatusMeaningWhat to do
grantedAllowedProceed
limitedPartial access (iOS photos)Proceed — you can see what the user selected
deniedRefused, but you may ask againExplain why you need it, then re-request
permanentlyDeniedThe OS will not show the prompt againSend the user to system settings
restrictedBlocked by policy or parental controlsDegrade gracefully; asking will never work
Dart
Future<void> _takePhoto() async {
  final outcome = await requestCamera();

  switch (outcome) {
    case PermissionOutcome.granted:
      await _openCamera();

    case PermissionOutcome.denied:
      _showMessage('Camera access is needed to attach a photo.');

    case PermissionOutcome.permanentlyDenied:
      final open = await _confirm(
        'Camera access is turned off. Open settings to enable it?',
      );
      if (open) await openAppSettings();

    case PermissionOutcome.restricted:
      _showMessage('Camera access is unavailable on this device.');
  }
}

Native configuration

android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- Android 13+ split the old storage permission by media type -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
ios/Runner/Info.plist
<key>NSCameraUsageDescription</key>
<string>FlutterLearn uses the camera so you can attach a photo to a note.</string>

<key>NSPhotoLibraryUsageDescription</key>
<string>FlutterLearn needs photo access so you can pick an existing image.</string>

<key>NSLocationWhenInUseUsageDescription</key>
<string>FlutterLearn uses your location to show nearby meetups.</string>
Dart
import 'package:image_picker/image_picker.dart';

final _picker = ImagePicker();

Future<File?> pickImage({required bool fromCamera}) async {
  final picked = await _picker.pickImage(
    source: fromCamera ? ImageSource.camera : ImageSource.gallery,
    // Resize on the way in — a 12MP photo is never what you want to upload
    maxWidth: 1600,
    imageQuality: 85,
    preferredCameraDevice: CameraDevice.rear,
  );

  if (picked == null) return null;   // user cancelled
  return File(picked.path);
}

// Multiple at once
final images = await _picker.pickMultiImage(maxWidth: 1600, imageQuality: 85);

Location

Dart
import 'package:geolocator/geolocator.dart';

Future<Position?> currentPosition() async {
  // Location has two gates: the OS service, and app permission
  if (!await Geolocator.isLocationServiceEnabled()) {
    await Geolocator.openLocationSettings();
    return null;
  }

  var permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
  }
  if (permission == LocationPermission.denied ||
      permission == LocationPermission.deniedForever) {
    return null;
  }

  return Geolocator.getCurrentPosition(
    locationSettings: const LocationSettings(
      accuracy: LocationAccuracy.medium,   // do not ask for best if you don't need it
      timeLimit: Duration(seconds: 10),
    ),
  );
}

// Continuous updates — remember to cancel the subscription
final subscription = Geolocator.getPositionStream(
  locationSettings: const LocationSettings(
    accuracy: LocationAccuracy.high,
    distanceFilter: 25,   // only emit after moving 25 metres
  ),
).listen(_onPosition);

LocationAccuracy.best keeps the GPS radio active and drains battery quickly. For "which city are you in", medium or even low is both faster and kinder. distanceFilter is the other big saving — without it you get an event every second whether the user moved or not.

Files and sharing

Dart
import 'package:file_picker/file_picker.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';

// Let the user choose a document
final result = await FilePicker.platform.pickFiles(
  type: FileType.custom,
  allowedExtensions: ['pdf', 'csv'],
);
final file = result?.files.single.path;

// Share text or files through the system sheet
await Share.shareXFiles(
  [XFile(exportPath)],
  text: 'My exported notes',
);

// Open a link, an email, or the dialer
final uri = Uri.parse('https://docs.flutter.dev');
if (await canLaunchUrl(uri)) {
  await launchUrl(uri, mode: LaunchMode.externalApplication);
}

Testing this properly

  • Test denial, not just approval — most bugs live in the path where the user says no.
  • Test permanent denial: deny twice on Android, or toggle the permission off in iOS Settings, and confirm your app offers the settings shortcut instead of silently doing nothing.
  • Test revocation while running: changing a permission in Settings restarts your Android app, which surfaces state-restoration bugs.
  • Test on a real device. Simulators fake the camera and report a fixed location.
  • Test with location services turned off entirely at the OS level — a different failure from permission denial.

Key takeaways

  • Handle all five permission states; permanentlyDenied needs a settings shortcut, not another request.
  • Ask in context at the moment of use — on iOS you get exactly one prompt.
  • Missing NS...UsageDescription strings crash iOS instantly.
  • Downscale picked images and copy them out of the cache directory if they must persist.

Practice

A note with an attachment

Build a note editor that can attach a photo from the camera or gallery and tag the note with the current city. Handle denial and permanent denial with distinct, useful UI, copy the image into permanent storage, and add a share button that exports the note.

Show hints
  • Write the permission wrapper once and reuse it — the switch on outcome should not be duplicated per feature.
  • Reverse-geocode with geocoding to turn coordinates into a city name.