image

02 Dec 2025

9K

35K

Flutter & Camera Plugin: Capturing Photos and Videos

Flutter, Google's UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, offers robust capabilities for integrating device-specific functionalities. One of the most frequently requested features in modern applications is the ability to capture photos and record videos directly within the app. The camera plugin is Flutter's official solution for seamlessly integrating camera functionalities.

This article will guide you through the process of setting up the Flutter camera plugin, initializing the camera, taking pictures, and recording videos, ensuring a professional and efficient implementation.

1. Setting Up the Environment

First, you need to add the camera plugin to your project's pubspec.yaml file.


dependencies:
  flutter:
    sdk: flutter
  camera: ^0.10.5+9 # Use the latest stable version
  path_provider: ^2.1.1 # Required for saving files
  path: ^1.8.3 # Required for path manipulation

After adding the dependency, run flutter pub get in your terminal.

Platform-Specific Configurations

To access the camera and storage, you need to declare appropriate permissions for both Android and iOS.

Android Configuration

Open android/app/src/main/AndroidManifest.xml and add the following permissions inside the <manifest> tag:


<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />

Note: For Android 10 (API level 29) and above, WRITE_EXTERNAL_STORAGE and READ_EXTERNAL_STORAGE might not be required if you're using scoped storage. The plugin typically handles this, but it's good practice to be aware.

iOS Configuration

Open ios/Runner/Info.plist and add the following keys to provide descriptions for camera and microphone usage. These messages will be displayed to the user when requesting permissions.


<key>NSCameraUsageDescription</key>
<string>This app needs access to your camera to take photos and videos.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to your microphone to record videos.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to your photo library to save photos and videos.</string>

2. Initializing the Camera

The core of camera functionality revolves around CameraController. Before using it, you need to find the available cameras on the device.


import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';

List<CameraDescription> cameras = [];

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  cameras = await availableCameras();
  runApp(const CameraApp());
}

class CameraApp extends StatefulWidget {
  const CameraApp({Key? key}) : super(key: key);

  @override
  State<CameraApp> createState() => _CameraAppState();
}

class _CameraAppState extends State<CameraApp> {
  CameraController? _controller;
  Future<void>? _initializeControllerFuture;

  @override
  void initState() {
    super.initState();
    // To display the current output from the Camera,
    // create a CameraController.
    _controller = CameraController(
      // Get a specific camera from the list of available cameras.
      cameras[0],
      // Define the resolution to use.
      ResolutionPreset.medium,
    );

    // Next, initialize the controller. This returns a Future.
    _initializeControllerFuture = _controller?.initialize();
  }

  @override
  void dispose() {
    // Dispose of the controller when the widget is disposed.
    _controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Camera Example')),
        body: FutureBuilder<void>(
          future: _initializeControllerFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              // If the Future is complete, display the preview.
              return CameraPreview(_controller!);
            } else {
              // Otherwise, display a loading indicator.
              return const Center(child: CircularProgressIndicator());
            }
          },
        ),
        floatingActionButton: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            FloatingActionButton(
              heroTag: 'captureImage',
              onPressed: () async {
                try {
                  await _initializeControllerFuture;
                  final XFile file = await _controller!.takePicture();
                  // Do something with the captured image file
                  print(file.path);
                } catch (e) {
                  print(e);
                }
              },
              child: const Icon(Icons.camera_alt),
            ),
            const SizedBox(height: 10),
            FloatingActionButton(
              heroTag: 'recordVideo',
              onPressed: () async {
                try {
                  await _initializeControllerFuture;
                  if (_controller!.value.isRecordingVideo) {
                    final XFile file = await _controller!.stopVideoRecording();
                    // Do something with the recorded video file
                    print(file.path);
                  } else {
                    await _controller!.startVideoRecording();
                  }
                  setState(() {}); // Update button state
                } catch (e) {
                  print(e);
                }
              },
              child: Icon(_controller!.value.isRecordingVideo ? Icons.stop : Icons.videocam),
            ),
          ],
        ),
      ),
    );
  }
}

In the above code:

  • availableCameras() asynchronously returns a list of CameraDescription objects, describing the available cameras.
  • We select the first camera (usually the rear camera) and initialize the CameraController with a desired resolution.
  • _controller?.initialize() prepares the camera for use. This method returns a Future that must be awaited before using the camera.
  • CameraPreview(_controller!) displays the live camera feed.
  • dispose() is crucial for releasing camera resources when the widget is no longer needed.

3. Taking Photos

Capturing a photo is straightforward once the camera controller is initialized. The takePicture() method returns an XFile object, which contains the path to the captured image.


Future<void> _takePicture() async {
  if (!_controller!.value.isInitialized) {
    return;
  }

  try {
    final XFile image = await _controller!.takePicture();
    // The image.path will contain the file path.
    // You can then display it, upload it, or save it permanently.
    print('Picture taken: ${image.path}');

    // Example: Navigate to a new screen to display the photo
    if (mounted) {
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => DisplayPictureScreen(
            imagePath: image.path,
          ),
        ),
      );
    }
  } catch (e) {
    print('Error taking picture: $e');
  }
}

// A simple screen to display the captured picture
class DisplayPictureScreen extends StatelessWidget {
  final String imagePath;

  const DisplayPictureScreen({Key? key, required this.imagePath}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Display the Picture')),
      body: Image.file(File(imagePath)),
    );
  }
}

You would integrate _takePicture() with a button press, as shown in the complete example above.

4. Recording Videos

Recording videos involves two main methods: startVideoRecording() and stopVideoRecording(). The latter returns an XFile representing the recorded video.


Future<void> _startVideoRecording() async {
  if (!_controller!.value.isInitialized || _controller!.value.isRecordingVideo) {
    return;
  }

  try {
    await _controller!.startVideoRecording();
    print('Video recording started');
    setState(() {}); // Update UI to reflect recording state
  } catch (e) {
    print('Error starting video recording: $e');
  }
}

Future<void> _stopVideoRecording() async {
  if (!_controller!.value.isRecordingVideo) {
    return;
  }

  try {
    final XFile video = await _controller!.stopVideoRecording();
    print('Video recorded: ${video.path}');
    setState(() {}); // Update UI to reflect non-recording state

    // Example: Navigate to a new screen to play the video (requires a video player plugin)
    // if (mounted) {
    //   Navigator.push(
    //     context,
    //     MaterialPageRoute(
    //       builder: (context) => DisplayVideoScreen(
    //         videoPath: video.path,
    //       ),
    //     ),
    //   );
    // }
  } catch (e) {
    print('Error stopping video recording: $e');
  }
}

The complete example earlier demonstrates how to toggle between starting and stopping video recording using a single Floating Action Button.

5. Error Handling and Lifecycle Management

Robust applications require proper error handling and lifecycle management. Always wrap camera operations in try-catch blocks. Furthermore, it's critical to dispose of the CameraController when it's no longer needed to prevent memory leaks and release camera resources.


@override
void dispose() {
  _controller?.dispose();
  super.dispose();
}

This ensures that the camera resources are freed up when your widget is removed from the widget tree.

6. Advanced Features and Considerations

  • Flash Modes: The camera plugin allows controlling the flash mode (e.g., auto, always on, torch, off) using _controller.setFlashMode().
  • Camera Switching: You can implement logic to switch between front and rear cameras by re-initializing the CameraController with a different CameraDescription from the availableCameras() list.
  • Resolution Presets: Experiment with different ResolutionPreset values (e.g., low, medium, high, max) to balance quality and file size according to your app's needs.
  • Preview Quality: For advanced cases, you can configure preview sizes manually.
  • Permissions: While platform configurations handle initial permission requests, consider using a package like permission_handler for runtime permission requests, especially for Android 6.0+ where users can revoke permissions.

Conclusion

The Flutter camera plugin provides a powerful yet intuitive API for integrating photo and video capture capabilities into your applications. By following the steps outlined in this article, you can successfully set up, configure, and utilize the device camera, offering rich multimedia experiences to your users. Remember to prioritize error handling and proper resource management for a stable and performant application.

Related Articles

Dec 19, 2025

Flutter & Firebase Auth: Seamless Social Media Login

Flutter & Firebase Auth: Seamless Social Media Login In today's digital landscape, user authentication is a critical component of almost every application. Pro

Dec 19, 2025

Building a Widget List with Sticky

Building a Widget List with Sticky Header in Flutter Creating dynamic and engaging user interfaces is crucial for modern applications. One common UI pattern th

Dec 19, 2025

Mastering Transform Scale & Rotate Animations in Flutter

Mastering Transform Scale & Rotate Animations in Flutter Flutter's powerful animation framework allows developers to create visually stunning and highly intera