skip to content

App States & Permissions

AppLifecycleListener reports resumed, inactive, hidden, paused and detached states, and permission_handler requests runtime permissions. Interviewers ask what to pause in the background.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

6

With Flutter's permission_handler package, how do you request microphone permission and handle each status the request can return?

level: juniorimportance: must knowfreq 52%

answer

  1. declare before you ask
  2. status reads, request() asks
  3. six PermissionStatus values
  4. permanentlyDenied means openAppSettings()
  5. Android status never permanentlyDenied

basics

~10 s

Declare 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 s

First 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 lines
dart
import '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

for a junior

Know the two declarations, the request() call and what granted, denied and permanentlyDenied each lead you to do.

for a middle

Explain the difference between status and request(), the iOS-only statuses, and why Android status no longer reports permanentlyDenied.

for a senior

Show a request-driven flow triggered by a user action, multi-permission requests, and no persisted verdicts after returning from settings.

for a principal

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.
open as a page

In Flutter, what are the five AppLifecycleState values, and what does each one mean on Android and iOS?

level: middleimportance: must knowfreq 58%

basics

~10 s

AppLifecycleState has resumed (visible, focused), inactive (visible, unfocused), hidden (no view visible), paused (not visible, no frames, mobile only) and detached (engine without a view, also the initial state).

open as a page

In a Flutter app targeting Android 15 or later, what does the default SystemUiMode.edgeToEdge change, and what stops working?

level: middleimportance: should knowfreq 26%

basics

~20 s

Edge-to-edge makes the app draw behind transparent status and navigation bars. On Android 15+ targets statusBarColor and systemNavigationBarColor stop working, other SystemUiModes need an opt-out, and Android 16 targets, the Flutter 3.47 default, cannot opt out.

open as a page

In Flutter, how does AppLifecycleListener differ from mixing WidgetsBindingObserver into a State to react to app lifecycle changes?

level: middleimportance: should knowfreq 38%

basics

~10 s

AppLifecycleListener, added in Flutter 3.13, is a disposable object that registers itself on WidgetsBinding and turns state changes into transition callbacks such as onHide, onShow and onExitRequested; WidgetsBindingObserver only hands you the new state.

open as a page

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%

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.

open as a page

In a Flutter video-call screen, at which AppLifecycleState should you release the camera, and what goes wrong if you also request permissions there?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Release the camera when the state becomes inactive and re-create it on resumed, as the camera plugin recommends. Never request permissions from that handler: the permission dialog itself makes the app inactive, so it loops.

open as a page