Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to create a custom camera for your FlutterFlow app

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.

What you'll learn

  • How to build a Custom Widget with live camera preview using the camera package
  • How to capture photos and switch between front and back cameras
  • How to handle camera lifecycle correctly to prevent 'Widget disposed' errors
  • How to upload captured images to Firebase Storage from a Custom Action
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner7 min read25-35 minFlutterFlow Pro+ (Custom Code required)March 2026RapidDev Engineering Team
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.

Prerequisites

  • 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.

custom_camera.dart
1import 'package:camera/camera.dart';
2import 'package:path_provider/path_provider.dart';
3import 'dart:io';
4
5class CustomCamera extends StatefulWidget {
6 const CustomCamera({super.key, this.width, this.height, this.onPhotoCaptured});
7 final double? width;
8 final double? height;
9 final Future Function(String filePath)? onPhotoCaptured;
10
11 @override
12 State<CustomCamera> createState() => _CustomCameraState();
13}
14
15class _CustomCameraState extends State<CustomCamera> with WidgetsBindingObserver {
16 CameraController? _controller;
17 List<CameraDescription> _cameras = [];
18 int _selectedCameraIndex = 0;
19 bool _isFlashOn = false;
20
21 @override
22 void initState() {
23 super.initState();
24 WidgetsBinding.instance.addObserver(this);
25 _initCamera();
26 }
27
28 Future<void> _initCamera() async {
29 _cameras = await availableCameras();
30 if (_cameras.isEmpty) return;
31 _controller = CameraController(_cameras[_selectedCameraIndex], ResolutionPreset.high);
32 await _controller!.initialize();
33 if (mounted) setState(() {});
34 }
35
36 @override
37 void didChangeAppLifecycleState(AppLifecycleState state) {
38 if (_controller == null || !_controller!.value.isInitialized) return;
39 if (state == AppLifecycleState.inactive) {
40 _controller?.dispose();
41 } else if (state == AppLifecycleState.resumed) {
42 _initCamera();
43 }
44 }
45
46 Future<void> _capturePhoto() async {
47 if (_controller == null || !_controller!.value.isInitialized) return;
48 final XFile photo = await _controller!.takePicture();
49 widget.onPhotoCaptured?.call(photo.path);
50 }
51
52 Future<void> _switchCamera() async {
53 _selectedCameraIndex = (_selectedCameraIndex + 1) % _cameras.length;
54 await _controller?.dispose();
55 _controller = CameraController(_cameras[_selectedCameraIndex], ResolutionPreset.high);
56 await _controller!.initialize();
57 if (mounted) setState(() {});
58 }
59
60 Future<void> _toggleFlash() async {
61 _isFlashOn = !_isFlashOn;
62 await _controller?.setFlashMode(_isFlashOn ? FlashMode.torch : FlashMode.off);
63 setState(() {});
64 }
65
66 @override
67 void dispose() {
68 WidgetsBinding.instance.removeObserver(this);
69 _controller?.dispose();
70 super.dispose();
71 }
72
73 @override
74 Widget build(BuildContext context) { /* see complete code */ }
75}

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 working example

custom_camera.dart
1import 'package:camera/camera.dart';
2
3class CustomCamera extends StatefulWidget {
4 const CustomCamera({
5 super.key, this.width, this.height, this.onPhotoCaptured,
6 });
7 final double? width;
8 final double? height;
9 final Future Function(String filePath)? onPhotoCaptured;
10
11 @override
12 State<CustomCamera> createState() => _CustomCameraState();
13}
14
15class _CustomCameraState extends State<CustomCamera>
16 with WidgetsBindingObserver {
17 CameraController? _controller;
18 List<CameraDescription> _cameras = [];
19 int _cameraIdx = 0;
20 bool _flash = false;
21
22 @override
23 void initState() {
24 super.initState();
25 WidgetsBinding.instance.addObserver(this);
26 _init();
27 }
28
29 Future<void> _init() async {
30 _cameras = await availableCameras();
31 if (_cameras.isEmpty) return;
32 _startCamera(_cameras[_cameraIdx]);
33 }
34
35 Future<void> _startCamera(CameraDescription cam) async {
36 _controller = CameraController(cam, ResolutionPreset.high);
37 await _controller!.initialize();
38 if (mounted) setState(() {});
39 }
40
41 @override
42 void didChangeAppLifecycleState(AppLifecycleState state) {
43 if (state == AppLifecycleState.inactive) _controller?.dispose();
44 if (state == AppLifecycleState.resumed) _init();
45 }
46
47 Future<void> _capture() async {
48 final xFile = await _controller!.takePicture();
49 widget.onPhotoCaptured?.call(xFile.path);
50 }
51
52 Future<void> _switch() async {
53 _cameraIdx = (_cameraIdx + 1) % _cameras.length;
54 await _controller?.dispose();
55 _startCamera(_cameras[_cameraIdx]);
56 }
57
58 Future<void> _toggleFlash() async {
59 _flash = !_flash;
60 await _controller?.setFlashMode(
61 _flash ? FlashMode.torch : FlashMode.off,
62 );
63 setState(() {});
64 }
65
66 @override
67 void dispose() {
68 WidgetsBinding.instance.removeObserver(this);
69 _controller?.dispose();
70 super.dispose();
71 }
72
73 @override
74 Widget build(BuildContext context) {
75 if (_controller == null || !_controller!.value.isInitialized) {
76 return const Center(child: CircularProgressIndicator());
77 }
78 return SizedBox(
79 width: widget.width ?? double.infinity,
80 height: widget.height ?? 500,
81 child: Stack(
82 children: [
83 Positioned.fill(child: CameraPreview(_controller!)),
84 Positioned(
85 top: 16, right: 16,
86 child: IconButton(
87 icon: Icon(_flash ? Icons.flash_on : Icons.flash_off,
88 color: Colors.white, size: 28),
89 onPressed: _toggleFlash,
90 ),
91 ),
92 Positioned(
93 top: 16, left: 16,
94 child: IconButton(
95 icon: const Icon(Icons.cameraswitch,
96 color: Colors.white, size: 28),
97 onPressed: _switch,
98 ),
99 ),
100 Positioned(
101 bottom: 32,
102 left: 0, right: 0,
103 child: Center(
104 child: GestureDetector(
105 onTap: _capture,
106 child: Container(
107 width: 72, height: 72,
108 decoration: BoxDecoration(
109 shape: BoxShape.circle,
110 border: Border.all(
111 color: Colors.white, width: 4),
112 color: Colors.white24,
113 ),
114 ),
115 ),
116 ),
117 ),
118 ],
119 ),
120 );
121 }
122}

Common mistakes when creating a custom camera for your FlutterFlow app

Why it's a problem: Forgetting to dispose the CameraController

How to avoid: Always call _controller?.dispose() in the widget's dispose() method AND when the app goes to background via WidgetsBindingObserver.didChangeAppLifecycleState.

Why it's a problem: Testing camera in iOS Simulator or Android Emulator

How to avoid: Always test camera features on a physical device. Use the simulator only for UI layout testing with a placeholder image.

Why it's a problem: Not handling the app lifecycle for camera

How to avoid: Implement WidgetsBindingObserver: dispose camera on AppLifecycleState.inactive, reinitialize on AppLifecycleState.resumed. This is the most critical pattern for camera widgets.

Why it's a problem: Capturing without checking controller.value.isInitialized

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Write a complete Flutter Custom Widget for FlutterFlow that shows a live camera preview with a capture button, front/back camera toggle, and flash toggle. Include proper lifecycle handling with WidgetsBindingObserver and an Action Parameter callback for when a photo is captured.

FlutterFlow Prompt

Create a page with a full-screen container for a custom camera widget. Add a back button at the top left. I will add the Custom Widget for camera preview manually in the container.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.