# How to create a custom camera for your FlutterFlow app

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 25-35 min
- Compatibility: FlutterFlow Pro+ (Custom Code required)
- Last updated: March 2026

## TL;DR

FlutterFlow has no built-in camera widget — this is the biggest content gap in the ecosystem. You need a Custom Widget using the camera package to show a live preview, capture photos, switch between front and back cameras, and toggle flash. This tutorial gives you the complete Dart code, explains lifecycle handling to avoid the common 'Widget disposed' crash, and shows how to upload captures to Firebase Storage.

## Building a camera widget from scratch in FlutterFlow

The camera is one of the most requested features in FlutterFlow, yet the platform has no built-in camera widget. Every camera tutorial online is fragmented into forum code snippets. This guide gives you a complete, production-ready Custom Widget with live preview, photo capture, front/back toggle, flash control, and proper lifecycle management. You will also connect it to Firebase Storage for automatic photo uploads.

## Before you start

- FlutterFlow Pro plan or higher (Custom Code required)
- A FlutterFlow project with Firebase connected
- Physical device for testing (camera does not work in simulators)
- Firebase Storage enabled in your Firebase project

## Step-by-step guide

### 1. Add camera and path_provider dependencies

Go to Custom Code → Pubspec Dependencies. Add two packages: camera (version ^0.10.5) for the camera hardware access and path_provider (version ^2.1.1) for temporary file storage of captured images. Both are required — camera captures frames, path_provider provides the temp directory to save photos before uploading.

**Expected result:** Both packages appear in Pubspec Dependencies without version conflicts.

### 2. Create the Custom Widget with CameraController

Create a new Custom Widget named CustomCamera. In initState(), get the list of available cameras via availableCameras(), select the back camera, then initialize CameraController with ResolutionPreset.high. Call controller.initialize() in an async method and setState when ready. In build(), return CameraPreview(controller) when initialized, or a loading indicator while initializing. Critically: implement WidgetsBindingObserver to handle app lifecycle — dispose camera when app goes to background, reinitialize when returning to foreground.

```
import 'package:camera/camera.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';

class CustomCamera extends StatefulWidget {
  const CustomCamera({super.key, this.width, this.height, this.onPhotoCaptured});
  final double? width;
  final double? height;
  final Future Function(String filePath)? onPhotoCaptured;

  @override
  State<CustomCamera> createState() => _CustomCameraState();
}

class _CustomCameraState extends State<CustomCamera> with WidgetsBindingObserver {
  CameraController? _controller;
  List<CameraDescription> _cameras = [];
  int _selectedCameraIndex = 0;
  bool _isFlashOn = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _initCamera();
  }

  Future<void> _initCamera() async {
    _cameras = await availableCameras();
    if (_cameras.isEmpty) return;
    _controller = CameraController(_cameras[_selectedCameraIndex], ResolutionPreset.high);
    await _controller!.initialize();
    if (mounted) setState(() {});
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (_controller == null || !_controller!.value.isInitialized) return;
    if (state == AppLifecycleState.inactive) {
      _controller?.dispose();
    } else if (state == AppLifecycleState.resumed) {
      _initCamera();
    }
  }

  Future<void> _capturePhoto() async {
    if (_controller == null || !_controller!.value.isInitialized) return;
    final XFile photo = await _controller!.takePicture();
    widget.onPhotoCaptured?.call(photo.path);
  }

  Future<void> _switchCamera() async {
    _selectedCameraIndex = (_selectedCameraIndex + 1) % _cameras.length;
    await _controller?.dispose();
    _controller = CameraController(_cameras[_selectedCameraIndex], ResolutionPreset.high);
    await _controller!.initialize();
    if (mounted) setState(() {});
  }

  Future<void> _toggleFlash() async {
    _isFlashOn = !_isFlashOn;
    await _controller?.setFlashMode(_isFlashOn ? FlashMode.torch : FlashMode.off);
    setState(() {});
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) { /* see complete code */ }
}
```

**Expected result:** The Custom Widget compiles. Camera preview appears when placed on a page and tested on a real device.

### 3. Build the camera overlay UI with capture and toggle buttons

In the build() method, return a Stack with the CameraPreview as the bottom layer and control buttons overlaid. Bottom center: large circular capture button (GestureDetector → Container with white border circle). Top right: flash toggle button (Icon toggles between flash_on and flash_off). Top left: camera switch button (Icon camera_flip). Use Positioned widgets inside the Stack for exact placement.

**Expected result:** Camera preview shows with capture, flash, and switch buttons overlaid.

### 4. Handle the captured photo with an Action Parameter callback

The onPhotoCaptured Action Parameter receives the file path of the captured image. In the parent page, wire this callback to your upload flow: read the file from the path, upload to Firebase Storage via a Custom Action using FirebaseStorage.instance.ref().child('photos/$userId/${DateTime.now()}.jpg').putFile(File(path)), then get the download URL and save it to your Firestore document.

**Expected result:** Captured photos upload to Firebase Storage and the download URL is saved in Firestore.

### 5. Request camera permission before showing the preview

Create a Custom Action named requestCameraPermission that uses the permission_handler package to check and request camera access. Call this action On Page Load before showing the camera widget. If permission denied, show an error message with a button that opens app settings via openAppSettings(). Add the permission_handler package to Pubspec Dependencies and add NSCameraUsageDescription to iOS Info.plist via Settings → Advanced → iOS Info.plist Entries.

**Expected result:** App requests camera permission on first visit and handles denial gracefully.

## Complete code example

File: `custom_camera.dart`

```dart
import 'package:camera/camera.dart';

class CustomCamera extends StatefulWidget {
  const CustomCamera({
    super.key, this.width, this.height, this.onPhotoCaptured,
  });
  final double? width;
  final double? height;
  final Future Function(String filePath)? onPhotoCaptured;

  @override
  State<CustomCamera> createState() => _CustomCameraState();
}

class _CustomCameraState extends State<CustomCamera>
    with WidgetsBindingObserver {
  CameraController? _controller;
  List<CameraDescription> _cameras = [];
  int _cameraIdx = 0;
  bool _flash = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _init();
  }

  Future<void> _init() async {
    _cameras = await availableCameras();
    if (_cameras.isEmpty) return;
    _startCamera(_cameras[_cameraIdx]);
  }

  Future<void> _startCamera(CameraDescription cam) async {
    _controller = CameraController(cam, ResolutionPreset.high);
    await _controller!.initialize();
    if (mounted) setState(() {});
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.inactive) _controller?.dispose();
    if (state == AppLifecycleState.resumed) _init();
  }

  Future<void> _capture() async {
    final xFile = await _controller!.takePicture();
    widget.onPhotoCaptured?.call(xFile.path);
  }

  Future<void> _switch() async {
    _cameraIdx = (_cameraIdx + 1) % _cameras.length;
    await _controller?.dispose();
    _startCamera(_cameras[_cameraIdx]);
  }

  Future<void> _toggleFlash() async {
    _flash = !_flash;
    await _controller?.setFlashMode(
      _flash ? FlashMode.torch : FlashMode.off,
    );
    setState(() {});
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_controller == null || !_controller!.value.isInitialized) {
      return const Center(child: CircularProgressIndicator());
    }
    return SizedBox(
      width: widget.width ?? double.infinity,
      height: widget.height ?? 500,
      child: Stack(
        children: [
          Positioned.fill(child: CameraPreview(_controller!)),
          Positioned(
            top: 16, right: 16,
            child: IconButton(
              icon: Icon(_flash ? Icons.flash_on : Icons.flash_off,
                  color: Colors.white, size: 28),
              onPressed: _toggleFlash,
            ),
          ),
          Positioned(
            top: 16, left: 16,
            child: IconButton(
              icon: const Icon(Icons.cameraswitch,
                  color: Colors.white, size: 28),
              onPressed: _switch,
            ),
          ),
          Positioned(
            bottom: 32,
            left: 0, right: 0,
            child: Center(
              child: GestureDetector(
                onTap: _capture,
                child: Container(
                  width: 72, height: 72,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    border: Border.all(
                      color: Colors.white, width: 4),
                    color: Colors.white24,
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}
```

## Common mistakes

- **Forgetting to dispose the CameraController** — The camera hardware stays active in the background, draining battery and preventing other apps from accessing the camera. FlutterFlow shows 'Unhandled Exception: Widget disposed' error. Fix: Always call _controller?.dispose() in the widget's dispose() method AND when the app goes to background via WidgetsBindingObserver.didChangeAppLifecycleState.
- **Testing camera in iOS Simulator or Android Emulator** — Simulators do not have camera hardware. The camera plugin throws 'CameraException: No cameras available' or shows a black screen. Fix: Always test camera features on a physical device. Use the simulator only for UI layout testing with a placeholder image.
- **Not handling the app lifecycle for camera** — When the user switches to another app and returns, the camera controller is in an invalid state. The preview freezes or crashes. Fix: Implement WidgetsBindingObserver: dispose camera on AppLifecycleState.inactive, reinitialize on AppLifecycleState.resumed. This is the most critical pattern for camera widgets.
- **Capturing without checking controller.value.isInitialized** — If the user taps capture before the camera finishes initializing, takePicture() throws an unhandled exception. Fix: Always check _controller != null && _controller!.value.isInitialized before calling takePicture(). Disable the capture button until initialized.

## Best practices

- Always implement WidgetsBindingObserver for camera lifecycle management
- Request camera permission before initializing the controller — use permission_handler package
- Add NSCameraUsageDescription in iOS Info.plist settings (required for App Store approval)
- Compress captured images before uploading — resize to max 1080px width to save storage and bandwidth
- Show a loading indicator while the camera initializes (takes 500ms-2s on first launch)
- Handle the case where no cameras are available gracefully (show error message)
- Test on both iOS and Android physical devices — camera behavior differs between platforms

## Frequently asked questions

### Why is there no built-in camera widget in FlutterFlow?

Camera hardware access requires platform-specific code and permissions that are difficult to abstract into a visual builder. FlutterFlow covers it via Custom Widgets, which gives you full control over the camera implementation.

### Can I record video as well as capture photos?

Yes. The camera package supports video recording via controller.startVideoRecording() and controller.stopVideoRecording(). You need to add recording state management and a timer display to the Custom Widget.

### Does the camera work on web?

The camera package has limited web support. For web, consider using the html package with getUserMedia API in a separate Custom Widget, or use the image_picker package which handles web camera capture via the browser's file picker.

### How do I add camera zoom?

Use controller.setZoomLevel(double). Add a Slider widget bound to zoom level (get min/max from controller.getMinZoomLevel() and controller.getMaxZoomLevel()). Pinch-to-zoom requires a GestureDetector with onScaleUpdate.

### How much does Firebase Storage cost for photo uploads?

Firebase Storage free tier: 5GB storage, 1GB/day download. Blaze plan: $0.026/GB stored, $0.12/GB downloaded. A typical compressed photo is 200-500KB, so 5GB holds roughly 10,000-25,000 photos.

### Can RapidDev help build advanced camera features?

Yes. If you need features like barcode scanning from camera, face detection overlays, or real-time image processing, RapidDev's team can build production-ready Custom Widgets with these capabilities.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-custom-camera-for-your-flutterflow-app
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-custom-camera-for-your-flutterflow-app
