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?
answer
- fresh install means configuration
- Android: manifest names lookup
- iOS: PERMISSION_* macros compiled out
- SPM reads Info.plist usage keys
- clear DerivedData after changes
basics
~20 sAn 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 sOn 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# 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
Remember that every permission must be declared in the Android manifest and in Info.plist before the request can show anything.
Explain how the plugin decides to skip the dialog on each platform and where its debug log tells you so.
Walk through the diagnosis order: clean install, merged manifest, usage key, SPM or Podfile macro, cache clearing, Xcode.app builds and flavors.
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.