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
Add camera and path_provider dependencies
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.
Create the Custom Widget with CameraController
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.
1import 'package:camera/camera.dart';2import 'package:path_provider/path_provider.dart';3import 'dart:io';45class 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;1011 @override12 State<CustomCamera> createState() => _CustomCameraState();13}1415class _CustomCameraState extends State<CustomCamera> with WidgetsBindingObserver {16 CameraController? _controller;17 List<CameraDescription> _cameras = [];18 int _selectedCameraIndex = 0;19 bool _isFlashOn = false;2021 @override22 void initState() {23 super.initState();24 WidgetsBinding.instance.addObserver(this);25 _initCamera();26 }2728 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 }3536 @override37 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 }4546 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 }5152 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 }5960 Future<void> _toggleFlash() async {61 _isFlashOn = !_isFlashOn;62 await _controller?.setFlashMode(_isFlashOn ? FlashMode.torch : FlashMode.off);63 setState(() {});64 }6566 @override67 void dispose() {68 WidgetsBinding.instance.removeObserver(this);69 _controller?.dispose();70 super.dispose();71 }7273 @override74 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.
Build the camera overlay UI with capture and toggle buttons
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.
Handle the captured photo with an Action Parameter callback
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.
Request camera permission before showing the preview
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
1import 'package:camera/camera.dart';23class 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;1011 @override12 State<CustomCamera> createState() => _CustomCameraState();13}1415class _CustomCameraState extends State<CustomCamera>16 with WidgetsBindingObserver {17 CameraController? _controller;18 List<CameraDescription> _cameras = [];19 int _cameraIdx = 0;20 bool _flash = false;2122 @override23 void initState() {24 super.initState();25 WidgetsBinding.instance.addObserver(this);26 _init();27 }2829 Future<void> _init() async {30 _cameras = await availableCameras();31 if (_cameras.isEmpty) return;32 _startCamera(_cameras[_cameraIdx]);33 }3435 Future<void> _startCamera(CameraDescription cam) async {36 _controller = CameraController(cam, ResolutionPreset.high);37 await _controller!.initialize();38 if (mounted) setState(() {});39 }4041 @override42 void didChangeAppLifecycleState(AppLifecycleState state) {43 if (state == AppLifecycleState.inactive) _controller?.dispose();44 if (state == AppLifecycleState.resumed) _init();45 }4647 Future<void> _capture() async {48 final xFile = await _controller!.takePicture();49 widget.onPhotoCaptured?.call(xFile.path);50 }5152 Future<void> _switch() async {53 _cameraIdx = (_cameraIdx + 1) % _cameras.length;54 await _controller?.dispose();55 _startCamera(_cameras[_cameraIdx]);56 }5758 Future<void> _toggleFlash() async {59 _flash = !_flash;60 await _controller?.setFlashMode(61 _flash ? FlashMode.torch : FlashMode.off,62 );63 setState(() {});64 }6566 @override67 void dispose() {68 WidgetsBinding.instance.removeObserver(this);69 _controller?.dispose();70 super.dispose();71 }7273 @override74 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation