With Flutter's permission_handler package, how do you request microphone permission and handle each status the request can return?
answer
- declare before you ask
- status reads, request() asks
- six PermissionStatus values
- permanentlyDenied means openAppSettings()
- Android status never permanentlyDenied
basics
~10 sDeclare RECORD_AUDIO in AndroidManifest.xml and NSMicrophoneUsageDescription in Info.plist, call Permission.microphone.request(), then branch: granted proceeds, denied can be asked again later, permanentlyDenied needs openAppSettings(), restricted cannot be changed.
solid answer
~30 sFirst declare the permission: `RECORD_AUDIO` in the Android manifest and `NSMicrophoneUsageDescription` in `Info.plist`. In Dart, `await Permission.microphone.request()` shows the system dialog if the OS still allows it and returns a `PermissionStatus`. `granted` means proceed; `denied` means ask again later after explaining why; `permanentlyDenied` means the dialog will not return, so offer `openAppSettings()`; `restricted` is iOS-only and cannot be changed by the user; `limited` and `provisional` apply to photos and notifications. Since `permission_handler_android` 14.1.0, `status` on Android never reports `permanentlyDenied`, so branch on the result of `request()`, called from a user action such as a "Join call" button.
code
dart · 27 linesimport 'package:permission_handler/permission_handler.dart';
enum MicAccess { ready, declined, blockedInSettings, unavailable }
Future<MicAccess> requestMicrophone() async {
final PermissionStatus status = await Permission.microphone.request();
return switch (status) {
PermissionStatus.granted || PermissionStatus.limited => MicAccess.ready,
PermissionStatus.denied => MicAccess.declined,
PermissionStatus.permanentlyDenied => MicAccess.blockedInSettings,
PermissionStatus.restricted || PermissionStatus.provisional => MicAccess.unavailable,
};
}
Future<void> onJoinCallPressed() async {
switch (await requestMicrophone()) {
case MicAccess.ready:
// start the call with audio
break;
case MicAccess.blockedInSettings:
await openAppSettings();
case MicAccess.declined:
case MicAccess.unavailable:
// join muted and explain why
break;
}
}go deeper
Know the two declarations, the request() call and what granted, denied and permanentlyDenied each lead you to do.
Explain the difference between status and request(), the iOS-only statuses, and why Android status no longer reports permanentlyDenied.
Show a request-driven flow triggered by a user action, multi-permission requests, and no persisted verdicts after returning from settings.
Discuss when to ask for permissions in the product journey and how the team keeps the Dart flow consistent with each platform's rules.
## What `permission_handler` is **`permission_handler`** is a federated Flutter plugin (13.0.2 at the time of writing, with `permission_handler_android` 14.1.0 underneath) that lets Dart code check and request **runtime permissions** such as the camera, microphone, location or contacts. Each permission is a constant on the `Permission` class, for example `Permission.microphone`, and extension methods add the operations you call. The plugin can only ask for what the app has **declared**. Before any Dart code runs: - **Android**: add `<uses-permission android:name="android.permission.RECORD_AUDIO"/>` to `android/app/src/main/AndroidManifest.xml`. - **iOS**: add an `NSMicrophoneUsageDescription` string to `ios/Runner/Info.plist`. With Swift Package Manager (the default since Flutter 3.44) the plugin compiles the microphone code in when that key is present; with CocoaPods you also enable the `PERMISSION_MICROPHONE=1` macro in the `Podfile`. ## Checking versus requesting | Call | What it does | Shows a dialog? | |---|---|---| | `Permission.microphone.status` | Reads the current `PermissionStatus` | Never | | `Permission.microphone.request()` | Asks if needed, returns the new status | Only if the OS still allows asking | | `[Permission.camera, Permission.microphone].request()` | Asks for several at once, returns `Map<Permission, PermissionStatus>` | For each permission the OS still allows asking | | `Permission.microphone.shouldShowRequestRationale` | Android hint that an explanation is advisable | Never; always `false` on iOS | | `openAppSettings()` | Opens this app's page in system settings | No, it leaves the app | If the permission is already granted, `request()` returns `granted` immediately without showing anything. ## The six statuses `PermissionStatus` has six values, and a Dart 3 `switch` over it must handle all of them: - **`granted`**: proceed. - **`denied`**: the user said no this time, or has not been asked yet. You may ask again later, ideally after explaining why. - **`permanentlyDenied`**: the OS will not show the dialog again; only the settings page can change it. Offer an "Open settings" action that calls `openAppSettings()`. - **`restricted`** (iOS only): the user cannot grant it, for example because of parental controls. - **`limited`**: partial access, today relevant to the photo library. - **`provisional`** (iOS only): quiet notification authorization. ## The Android status change in `permission_handler_android` 14.1.0 On Android, **`status` never returns `permanentlyDenied`** any more; it reports `denied` for every denied permission. Only the result of `request()` can be `permanentlyDenied`. The reason is that Android offers no API to tell "never asked", "reset to Ask every time" and "permanently denied" apart, and the old guess sent some users to settings when a dialog would have appeared. Requesting a permanently denied permission is cheap: it resolves at once without a dialog. iOS is unchanged. The resulting pattern works on both platforms: 1. Use `status` only to skip work when the permission is already `granted`, or to decide whether to show your own explainer first. 2. Call `request()` from a **user action**, such as tapping "Join call". 3. Branch on the **result** of `request()`. 4. Never persist a `permanentlyDenied` verdict; after the user returns from settings, check `status` again. ## Practical rules - Only one request may run at a time; a second concurrent call fails with an "already running" error, so request several permissions in one list call instead. - Ask in context. A microphone prompt at app start, with no call in sight, is more likely to be declined. - After an `await`, check `mounted` before touching `BuildContext` or calling `setState`. ## Common mistakes 1. **Asking at startup.** Requesting every permission in `main` or the first screen's `initState`, before the user knows why, invites refusals the app then has to recover from through settings. 2. **Treating `denied` as final.** `denied` still allows a later request; only `permanentlyDenied` requires the settings page. 3. **Ignoring the iOS-only values.** An exhaustive `switch` forces a decision for `restricted` and `provisional`; a chain of `if (status.isGranted)` checks silently lumps them in with refusals. 4. **Forgetting the declarations.** Without the manifest entry or the usage-description key, `request()` can come back `denied` without ever showing a dialog, which looks exactly like a user refusal. 5. **Asking again in a loop.** Re-requesting straight after a refusal annoys users and, once the OS stops showing the dialog, achieves nothing.
- How do you ask for camera and microphone together for a video call?Call `request()` on a list: `await [Permission.camera, Permission.microphone].request()` returns a `Map<Permission, PermissionStatus>`. Look up each entry and handle them separately, for example join with video off if only the microphone was granted. Do not fire two separate `request()` calls at once; the plugin rejects a request while another is running.
- What is shouldShowRequestRationale for, and why does it return false on iOS?On Android it reports whether the OS suggests showing your own explanation before asking again, typically after an earlier denial. `permission_handler` implements it only on Android and returns `false` on every other platform, so treat it as a hint for Android copy, not as a cross-platform status.
saying these in an interview costs you the question
- request() works without any manifest or Info.plist entry.
- On Android, checking status is enough to detect a permanently denied permission.
- A denied result means you must immediately send the user to settings.
- Calling request() on a permanently denied permission shows the dialog again.
- Storing permanentlyDenied locally is fine to avoid asking again.