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
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,
};
}| Status | Meaning | What to do |
|---|---|---|
granted | Allowed | Proceed |
limited | Partial access (iOS photos) | Proceed — you can see what the user selected |
denied | Refused, but you may ask again | Explain why you need it, then re-request |
permanentlyDenied | The OS will not show the prompt again | Send the user to system settings |
restricted | Blocked by policy or parental controls | Degrade gracefully; asking will never work |
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
<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" /><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>Images: camera and gallery
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
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
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;
permanentlyDeniedneeds a settings shortcut, not another request. - Ask in context at the moment of use — on iOS you get exactly one prompt.
- Missing
NS...UsageDescriptionstrings 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
geocodingto turn coordinates into a city name.