skip to content

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%

answer

  1. camera plugin stopped handling lifecycle
  2. release early, restore on resumed
  3. inactive covers app switcher, calls
  4. permission dialog triggers inactive
  5. check status, do not request

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.

solid answer

~40 s

Since `camera` 0.5.0 the plugin leaves lifecycle to you, and its README disposes the `CameraController` on `inactive` and re-initialises it on `resumed`. `inactive` is the first step towards the background and also covers the app switcher, calls and split screen, where another app may claim the camera while yours is still visible; `paused` may come late or, after a kill, never. The trap is that a system permission dialog takes focus, so it moves the app to `inactive` and back to `resumed`. A handler that re-initialises and requests permission on `resumed` disposes and re-requests in a loop. Request from a user action, and on `resumed` only check `Permission.microphone.status` before re-initialising.

code

dart · 37 lines
dart
import 'package:camera/camera.dart';
import 'package:flutter/widgets.dart';
import 'package:permission_handler/permission_handler.dart';

class CallCamera {
  CallCamera(this._description) {
    _lifecycle = AppLifecycleListener(
      onInactive: _release,
      onResume: _restoreIfAllowed,
    );
  }

  final CameraDescription _description;
  late final AppLifecycleListener _lifecycle;
  CameraController? _controller;

  Future<void> _release() async {
    final CameraController? controller = _controller;
    _controller = null;
    await controller?.dispose();
  }

  Future<void> _restoreIfAllowed() async {
    // Check, never request: a request dialog would itself trigger inactive.
    if (!await Permission.camera.status.isGranted || _controller != null) {
      return;
    }
    final CameraController controller = CameraController(_description, ResolutionPreset.medium);
    _controller = controller;
    await controller.initialize();
  }

  void dispose() {
    _lifecycle.dispose();
    _release();
  }
}

go deeper

for a junior

Remember that the camera must be released when the app stops being in front and set up again when it returns.

for a middle

Explain why the camera plugin example disposes on inactive and re-initialises on resumed, and why the controller may be uninitialised when a change arrives.

for a senior

Diagnose the dialog-induced inactive-resumed loop, and show a handler that checks status instead of requesting and guards in-flight work and mounted.

for a principal

Weigh what a call keeps running in the background against platform policies and battery cost, and where that decision lives across Android, iOS and desktop builds.

## The scenario A video-call screen in a Flutter app holds two scarce resources: the **camera**, usually through a `CameraController` from the `camera` package, and the **microphone**. Both belong to the operating system, which can hand them to another app the moment yours is no longer in front. The interview question is really: *at which lifecycle step do you let go, and how do you come back cleanly?* ## Why `inactive`, not `paused` The `camera` plugin stopped handling lifecycle changes itself in version 0.5.0; its README now tells you to do it and shows the pattern: **dispose the controller when the state becomes `inactive`, and re-create it when it becomes `resumed`.** Reasons to act at `inactive` rather than waiting for `paused`: - **`inactive` comes first.** Going to the background delivers `inactive`, then `hidden`, then `paused`. Releasing at the first step gives the OS the device as early as possible. - **Other apps may take the camera while you are still visible.** On iOS, `inactive` covers the app switcher, Control Center and an incoming phone call; on Android it covers split screen, picture-in-picture and system dialogs. In several of those cases the app never reaches `paused`. - **`paused` may never arrive.** A killed process gets no notification, so work parked until `paused` or `detached` can be lost. - **Desktop and web never report `paused`.** If the same screen runs there, only `hidden` or `inactive` fire. Deciding what continues in the background (for example keeping the audio of a call alive) is a product choice that also needs platform background-mode configuration; Flutter's lifecycle API only tells you when the transition happens. ## The trap: the permission dialog is itself a lifecycle event The same `inactive` rule creates the classic bug on a video-call screen. A system permission dialog takes input focus, so **showing it moves the app to `inactive`**, and dismissing it moves it back to `resumed`. Consider this sequence: 1. `resumed`: the screen initialises the camera and requests microphone permission. 2. The OS dialog appears: `inactive`, so the handler disposes the camera. 3. The user answers: `resumed`, so the handler re-initialises and requests again. 4. Back to step 2, or at best a flickering, re-initialising preview. Fixes, in order of preference: - **Separate "ask" from "resume".** Request permissions from an explicit user action (the "Join call" button) and never from the lifecycle handler. - **On `resumed`, check rather than request.** Read `Permission.microphone.status`; if it is granted, re-initialise; otherwise leave the call muted and wait for the user. - **Guard in-flight work.** Keep a flag such as `_requestingPermission` and ignore lifecycle-driven re-initialisation while it is set; `permission_handler` also rejects a second concurrent request with an "already running" error. - **Check the controller before touching it.** Return early if it is null or not yet initialised, exactly as the `camera` example does, because a state change can arrive before initialisation finishes. ## A resource table for the call screen | Resource | Release at | Restore at | Why | |---|---|---|---| | `CameraController` | `inactive` | `resumed` | OS may reassign the camera while you are visible | | Outgoing video track | `hidden` | `inactive` (show) | Nobody sees a hidden preview; works on every platform | | Microphone | product decision | `resumed` | Background audio needs platform configuration | | Frame-driven animations | nothing to do | nothing to do | The engine schedules no frames while `paused` | ## Checklist for the handler 1. Register one listener (or observer) per screen, and dispose it with the screen. 2. Release the camera on `inactive`; re-create it on `resumed` using the previous `CameraDescription`. 3. Never call `request()` from the lifecycle handler; check `status` instead. 4. After every `await`, check `mounted` before calling `setState` or using `context`. 5. Assume any step can be skipped, and save call state (who, since when, muted or not) as soon as the call starts.

  • Why not just pause the preview with CameraController.pausePreview() on inactive instead of disposing?
    `pausePreview()` stops the preview but keeps the controller initialised, so the app still holds the camera. That is fine for a brief in-app overlay, but when the OS may hand the camera to another app, releasing it with `dispose()` is what the `camera` README recommends.
  • The same call screen also ships on Flutter desktop. What changes in the lifecycle handling?
    Desktop never reports `paused`, so any work tied to `onPause` never runs. Use `hidden` (via `onHide` and `onShow`) for minimise and restore, and `inactive` for focus loss, which on desktop only means another window has focus while yours is visible.

saying these in an interview costs you the question

  • The camera plugin releases the camera automatically when the app goes to the background.
  • Waiting for paused is safe because it always arrives before another app opens the camera.
  • Re-requesting permission on every resumed is harmless whatever the current status is.
  • A permission dialog does not change the app's lifecycle state.
  • Releasing hardware in the detached handler is enough.