skip to content

A Flutter app's permission_handler call Permission.camera.request() returns denied instantly with no dialog on a fresh install; how do you diagnose it?

level: seniorimportance: should knowfreq 30%

answer

  1. fresh install means configuration
  2. Android: manifest names lookup
  3. iOS: PERMISSION_* macros compiled out
  4. SPM reads Info.plist usage keys
  5. clear DerivedData after changes

basics

~20 s

An instant denied on a fresh install almost always means the permission is not declared: the Android manifest lacks the uses-permission entry, or on iOS the PERMISSION_CAMERA macro is off because NSCameraUsageDescription is missing or the Podfile does not enable it.

solid answer

~30 s

On a fresh install the user has refused nothing, so suspect configuration. On Android, `permission_handler` looks up manifest entries for the permission; with no `<uses-permission android:name="android.permission.CAMERA"/>` it returns `denied` without asking and logs "No permissions found in manifest". On iOS each permission is compiled behind a `PERMISSION_*` macro, and a compiled-out permission reports `denied`. Under Swift Package Manager, the default since Flutter 3.44, the macro is on only when `NSCameraUsageDescription` is in an `Info.plist` the package can find; the result is cached, so clear DerivedData, and builds started from Xcode.app need `PERMISSION_HANDLER_INFO_PLIST`. Under CocoaPods the `Podfile` must set `PERMISSION_CAMERA=1`.

code

bash · 9 lines
bash
# Android: read the plugin's debug log while reproducing
adb logcat | grep -i "permissions found in manifest"

# iOS (Swift Package Manager): after adding NSCameraUsageDescription
rm -rf ~/Library/Developer/Xcode/DerivedData
flutter build ios

# Builds started from Xcode.app cannot find Info.plist on their own
launchctl setenv PERMISSION_HANDLER_INFO_PLIST "$PWD/ios/Runner/Info.plist"

go deeper

for a junior

Remember that every permission must be declared in the Android manifest and in Info.plist before the request can show anything.

for a middle

Explain how the plugin decides to skip the dialog on each platform and where its debug log tells you so.

for a senior

Walk through the diagnosis order: clean install, merged manifest, usage key, SPM or Podfile macro, cache clearing, Xcode.app builds and flavors.

for a principal

Decide how flavors declare different permission sets and how the pipeline verifies a production binary carries only the permissions it asks for.

## The symptom A Flutter app calls `await Permission.camera.request()` from `permission_handler` on a **fresh install**. No dialog appears, and the call returns **`denied`** immediately. On a fresh install the user cannot have denied anything yet, so the cause is almost always **configuration**: the plugin concluded that the app never declared the permission and refused without asking. ## Android: the manifest entry On Android, before requesting, the plugin looks up the manifest names for the requested permission in the **merged app manifest**. If it finds none, it records `denied` for that permission and skips the request. The plugin logs a debug-level line such as `No permissions found in manifest for: ...` under its log tag, which is the fastest confirmation. Check: - `android/app/src/main/AndroidManifest.xml` has `<uses-permission android:name="android.permission.CAMERA"/>` for the camera (or `RECORD_AUDIO` for the microphone). - The entry is in the `main` manifest. The plugin README notes there are `debug`, `main` and `profile` manifests; an entry only in `debug` disappears from other builds. - A flavor-specific manifest did not replace or omit it. ## iOS: the compiled-in permission On iOS the plugin compiles each permission's native code behind a **`PERMISSION_*` preprocessor macro**, so that an app does not reference permission APIs it has no usage description for. A permission that is compiled out reports `denied`. How the macro is set depends on the dependency manager: | Setup | Who enables `PERMISSION_CAMERA` | Typical cause of `denied` | |---|---|---| | Swift Package Manager (default since Flutter 3.44) | The package manifest, when `NSCameraUsageDescription` is present in an `Info.plist` it can find | Key missing; manifest cached; build started from Xcode.app, which cannot locate the `Info.plist` | | CocoaPods | Your `Podfile` `post_install` block, in `GCC_PREPROCESSOR_DEFINITIONS` | Macro missing or set to `0` | Details that trip teams up under Swift Package Manager: 1. **Caching.** The package manifest is not re-evaluated when `Info.plist` changes. After adding the key, clear `~/Library/Developer/Xcode/DerivedData` and rebuild. 2. **Builds from Xcode.app.** They run with `/` as the working directory, so automatic discovery fails and **every** permission is compiled out. Point the plugin at the file with `launchctl setenv PERMISSION_HANDLER_INFO_PLIST /path/to/Info.plist` and restart Xcode. 3. **Seeing what happened.** Setting `PERMISSION_HANDLER_VERBOSE=1` and dumping the package logs which `Info.plist` files were read and what every macro resolved to. 4. **Flavors.** Keys from all configurations are merged, so a permission declared only for `dev` is compiled into `prod` too. A `permission_handler.yaml` next to `pubspec.yaml` gives each flavor its own `Info.plist`, selected with `dart run permission_handler_apple:select <flavor>`. ## Other causes that look similar - **Not a fresh install.** On a device where the user already refused, `request()` can return `permanentlyDenied` instantly, which is correct behaviour, not misconfiguration. - **No Activity yet.** Requesting before the Android Activity is attached fails with an "Unable to detect current Android Activity" error, a `PlatformException` rather than `denied`. - **Overlapping requests.** A second request while one is running fails with an "already running" error; batch permissions into one list call. ## A diagnosis order 1. Reproduce on a clean install and read the plugin's debug log on Android. 2. Confirm the manifest entry in the **merged** manifest of the variant you built. 3. On iOS, confirm the usage-description key, then how the macro is set (SPM or `Podfile`), and clear DerivedData. 4. Only then suspect the Dart code, such as a request made before `runApp` or two requests at once. ## Preventing it - **Declare in the same change as the request.** Every new `Permission.x.request()` in Dart ships with its manifest entry and its `Info.plist` usage key, and code review checks the three together. - **Build release-like variants in CI**, not only debug, so a permission declared only in a debug manifest or only in a dev `Info.plist` shows up before users do. - **Give each flavor its own `Info.plist`** through `permission_handler.yaml` when flavors genuinely need different permissions, instead of relying on merged keys. - **Keep the verbose switch handy.** `PERMISSION_HANDLER_VERBOSE=1` turns an invisible compile-time decision into a readable list of resolved macros.

  • The iOS build works from flutter run but every permission reports denied when built from Xcode.app. Why?
    Under Swift Package Manager the package finds `Info.plist` through the build's working directory. `flutter run` and `flutter build ios` point it at the project; Xcode.app runs from `/`, so nothing is found and every permission is compiled out. Set `PERMISSION_HANDLER_INFO_PLIST` with `launchctl setenv`, restart Xcode and clear DerivedData.
  • Why does permission_handler compile iOS permissions out instead of including them all?
    Referencing a permission API without a matching usage description gets an app rejected at submission, so each permission's native code sits behind a macro. Under Swift Package Manager only permissions with a usage key are compiled in, which is why a missing key silently produces `denied`.

saying these in an interview costs you the question

  • A fresh install returning denied means the user tapped Don't allow.
  • Adding the Info.plist key takes effect on the next build without clearing caches.
  • A uses-permission entry in the debug manifest covers release builds too.
  • Instant denied is a Dart bug, so the fix belongs in the request code.
  • iOS builds include every permission's code whether declared or not.