Building a Camera App with Flutter and the Camera Package
Tags: beginners, flutter, tutorial
Published: 2023-03-20 15:54:37

## Introduction

The **`camera`** package is a Dart package that provides an interface for accessing the device's camera. It's a popular package for building camera apps and is maintained by the Flutter team. The **`camera`** package provides a lot of functionality, including displaying the camera preview, capturing photos and videos, adjusting camera settings, and more.

In this article, we will explore the Camera package in Flutter to build a camera app. We will cover the basics of capturing photos and videos using the camera package.

## **Advantages of Using the Camera Package**

There are several advantages to using the **`camera`** package for building camera apps with Flutter. Here are some of the main benefits:

- **Ease of use**: The **`camera`** package provides a simple and easy-to-use interface for accessing the device's camera, making it easy for developers to build camera apps quickly.
- **Cross-platform compatibility**: Since Flutter is a cross-platform framework, apps built with the **`camera`** package can run on both Android and iOS platforms.
- **Performance**: The **`camera`** package is optimized for performance, providing a smooth and responsive camera experience for users.
- **Customization**: The **`camera`** package provides a lot of customization options, allowing developers to adjust camera settings, add filters and effects, and more.

## **Prerequisites**

Before we begin, make sure you have the following prerequisites:

- Flutter installed on your machine
- An Android or iOS device connected to your machine

Not yet installed flutter?, check out my **Getting Started with Flutter**, everything you need to install flutter is explained there. 

{% embed https://dev.to/bishopeze/getting-started-with-flutter-415m %}

Let's refocus on the subject of this article and start developing our camera app.

## **Building the Camera App**

Let's begin building our camera app with the help of Flutter and the camera package. Here are the steps to get you started.

## **Setup**

Create a new Flutter app by running the following command:

```bash
flutter create flutter_camera_app
```

This will create a new Flutter project named **`flutter_camera_app`**.

### **Add the `camera` and `video_player` package to your project**

The **`video_player`** package is responsible for rendering captured videos on the screen. We will be using it to display recorded videos from the **`camera`** plugin.

To use these packages in your Flutter project, you need to add them to your project's **`pubspec.yaml`** file. Here's an example:

```yaml
dependencies:
  camera: ^0.10.0
	video_player: ^2.5.2
```

Once you have added this to your **`pubspec.yaml`** file, open a terminal on VSCode or your code editor, and run this command **`flutter pub get`** 

This will download and install the respective packages and their dependencies.

### **Import the packages**

After adding the **`camera`** and **`video_player`** package to your project, you need to import it into your Flutter code. Add the following import statement to your code:

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

### Implement the root widget of our app

To configure the home screen of our app, we set the root widget to display the **`CameraPage`**
 widget and apply a dark theme for a more visually appealing experience.

```dart
void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Camera App',
      themeMode: ThemeMode.dark,
      theme: ThemeData.dark(),
      debugShowCheckedModeBanner: false,
      home: const CameraPage(),
    );
  }
}
```

### Implement the `CameraPage` widget class

The **`CameraPage`** widget class is responsible for handling critical tasks like previewing the live camera feed, capturing photos, and recording videos in our application. To configure this widget class to effectively fulfill these tasks, we'll convert it into a stateful widget and set up the required parameters for the camera package within the state class to ensure seamless operations.

```dart
class CameraPage extends StatefulWidget {
  const CameraPage({super.key});

  @override
  CameraPageState createState() => CameraPageState();
}

class CameraPageState extends State<CameraPage> with WidgetsBindingObserver {
  CameraController? _controller;
  bool _isCameraInitialized = false;
  late final List<CameraDescription> _cameras;
  bool _isRecording = false;

	@override
	build(BuildContext context){
		return Container();
	}
}
```

You may be curious why we added the **`WidgetsBindingObserver`** mixin to the state class of the **`CameraPage`** widget. As we continue with the article, we will provide further explanation on this.

In our implementation, the **`CameraController`** property serves as the controller for the camera, responsible for calling all camera-specific methods such as previewing the camera, taking photos, and recording videos.

We also utilize two boolean flags, **`_isCameraInitialized`** and **`_isRecording`**, to manage state changes and update the UI in response to events from the **`_controller`** property.

Lastly, the **`List<CameraDescription> _cameras`** property is a crucial component of this widget class, returning a list of available cameras on the device where the app is being launched.

### **Initializing the camera**

To gain access to the camera preview and initialize the camera, the next step involves calling the **`availableCameras()`** method from the camera package. This method returns a list of available cameras, which you can utilize to enable camera functionality within your application.

```dart
Future<void> initCamera() async {
    _cameras = await availableCameras();
    // Initialize the camera with the first camera in the list
    await onNewCameraSelected(_cameras.first);
  }
```

This method is responsible for getting a  list of available cameras from the device and then passing the first camera available on the list which is the back camera to the **`onNewCameraSelected`** which is responsible for initializing this camera using the **`_controller`** property. The implementation of this **`onNewCameraSelected`** method is shown below.

```dart
Future<void> onNewCameraSelected(CameraDescription description) async {
    final previousCameraController = _controller;

    // Instantiating the camera controller
    final CameraController cameraController = CameraController(
      description,
      ResolutionPreset.high,
      imageFormatGroup: ImageFormatGroup.jpeg,
    );

    // Initialize controller
    try {
      await cameraController.initialize();
    } on CameraException catch (e) {
      debugPrint('Error initializing camera: $e');
    }
    // Dispose the previous controller
    await previousCameraController?.dispose();

    // Replace with the new controller
    if (mounted) {
      setState(() {
        _controller = cameraController;
      });
    }

    // Update UI if controller updated
    cameraController.addListener(() {
      if (mounted) setState(() {});
    });

    // Update the Boolean
    if (mounted) {
      setState(() {
        _isCameraInitialized = _controller!.value.isInitialized;
      });
    }
  }
```

In the above code, **`previousCameraController`** is related to the **`AppLifecycleState`** changes and the **`WidgetsBindingObserver`** mixin, which we will discuss in more detail shortly. Moving to the next code block, we begin by instantiating a **`cameraController`** with the **`cameraDescription`** parameter, followed by setting the camera's resolution to **`Resolution.high`** to achieve clear camera feedback in the camera preview.

Afterwards, we proceed to initialize the **`cameraController`**, wrapping it in a try-catch block to handle any possible exceptions that may arise. Upon successful initialization, we then reassign the **`_controller`** property to this new **`cameraController`** object, using the **`setState`** method and ensuring that the widget class is **`mounted`** before doing so.

The **`cameraController.addListener((){})`** function listens for updates to the controller object, with **`setState`** being called within the callback to recall the **`build`** method and render the camera preview seamlessly. This method is closely linked with the **`AppLifecycleStates`** and **`WidgetsBindingObserver`**. Finally, in the **`dispose`** method, we make sure to dispose of the **`_controller`** to properly clean up the stateful widget whenever the app is minimized or killed.

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

To prevent the **`camera`** plugin from throwing an exception when returning to the app with a disposed controller, we need to handle the **`AppLifecycleStates`**. We can achieve this by adding the **`WidgetsBindingObserver`** mixin to our **`CameraPage`** state class, as shown below.

```dart
class CameraPage extends StatefulWidget {
  const CameraPage({super.key});

  @override
  CameraPageState createState() => CameraPageState();
}

class CameraPageState extends State<CameraPage> with WidgetsBindingObserver {

@override
  void didChangeAppLifecycleState(AppLifecycleState state) {
   final CameraController? cameraController = _controller;

		 if (cameraController == null || !cameraController.value.isInitialized) {
      return;
    }

    if (state == AppLifecycleState.inactive) {
      // Free up memory when camera not active
      cameraController.dispose();
    } else if (state == AppLifecycleState.resumed) {
      // Reinitialize the camera with same properties
      onNewCameraSelected(cameraController.description);
    }
  }
```

By adding the **`WidgetsBindingObserver`** mixin to the **`CameraPage`** state class, we gain access to the **`didChangeAppLifecycleState`** method to handle the **`AppLifecycleStates`**. We check if the **`cameraController`** is null or has not been initialized, and if so, we exit the function. When the app is inactive, we dispose of the non-null camera controller to free up memory, and when the app is resumed, we call the **`onNewCameraSelected`** method to handle the disposal and reinitialization of the previous and new controllers.

### Displaying the camera preview

After successfully initializing the camera, the next step is to display the camera preview in the application's UI. This is an important step as it allows the user to interact with the camera and view the live feed.

```dart
@override
  Widget build(BuildContext context) {
    if (_isCameraInitialized) {
      return SafeArea(
        child: Scaffold(
          body: Column(
            children: [
              CameraPreview(_controller!),
	             ...
							]
      );
    } else {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }
  }
```

The **`camera`** plugin provides a useful **`CameraPreview`** widget that takes in our controller as an argument and renders the visual feedback coming in from our initialized camera. The **`_isCameraInitialized`** boolean flag, which is set to true or false in the **`onNewCameraSelected`** method, determines whether the camera has been initialized successfully or not. If the camera is not initialized, our UI renders a **`CircularProgressIndicator`** until the camera has been initialized.

Here's an example of how the camera preview appears on a mobile device:

![Camera Preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uddt0401dtwc3f7insp7.jpg)

### Capture photos and videos

With the camera preview successfully displayed, we can now move on to implementing the photo capture feature in our application. This involves utilizing the **`camera`** plugin's **`CameraController`**
 to capture images from the camera.

```dart
Future<XFile?> capturePhoto() async {
    final CameraController? cameraController = _controller;
    if (cameraController!.value.isTakingPicture) {
      // A capture is already pending, do nothing.
      return null;
    }
    try {
      await cameraController.setFlashMode(FlashMode.off); //optional
      XFile file = await cameraController.takePicture();
      return file;
    } on CameraException catch (e) {
      debugPrint('Error occured while taking picture: $e');
      return null;
    }
  }

	void _onTakePhotoPressed() async {
    final navigator = Navigator.of(context);
    final xFile = await capturePhoto();
    if (xFile != null) {
      if (xFile.path.isNotEmpty) {
        navigator.push(
          MaterialPageRoute(
            builder: (context) => PreviewPage(
              imagePath: xFile.path,
            ),
          ),
        );
      }
    }
  }
```

In order to capture photos in our application, we will make use of two helper methods. The first method will be responsible for taking the photo and saving it to a file, while the second will handle displaying the captured photo on a screen.

The **`capturePhoto()`** method initializes a **`cameraController`** and assigns it to our **`_controller`**. We then check if the camera is currently taking pictures, and exit the method if it is. After this check, we call **`cameraController.takePicture()`** and assign the returned data to an **`Xfile`** object, which is then returned from the method. This entire process is wrapped in a try-catch block to handle potential exceptions.

When **`_onTakePhotoPressed()`** is called from the UI, we await the **`capturePhoto()`** method which returns an **`XFile`**. If the file is not null and not empty, we push our new screen, **`PreviewPage`**, onto the navigation stack and pass the photo as a parameter.

The code snippet below shows an example of how this method is called in the UI:

```dart
@override
  Widget build(BuildContext context) {
    if (_isCameraInitialized) {
      return SafeArea(
        child: Scaffold(
          body: Column(
            children: [
	            ....
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  if (!_isRecording)
                    ElevatedButton(
                      onPressed: _onTakePhotoPressed,
                      style: ElevatedButton.styleFrom(
                          fixedSize: const Size(70, 70),
                          shape: const CircleBorder(),
                          backgroundColor: Colors.white),
                      child: const Icon(
                        Icons.camera_alt,
                        color: Colors.black,
                        size: 30,
                      ),
		               ....
                ],
              ),
            ],
          ),
        ),
      );
    } else {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }
  }
```

The image below shows a sample photograph taken by the app.


![Sample Photo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2cpcb46xbqktxtcz3725.jpg)

Now to record videos in our application, we make use of these helper methods.

```dart
Future<XFile?> captureVideo() async {
    final CameraController? cameraController = _controller;
    try {
      setState(() {
        _isRecording = true;
      });
      await cameraController?.startVideoRecording();
      await Future.delayed(const Duration(seconds: 5));
      final video = await cameraController?.stopVideoRecording();
      setState(() {
        _isRecording = false;
      });
      return video;
    } on CameraException catch (e) {
      debugPrint('Error occured while taking picture: $e');
      return null;
    }
  }

void _onRecordVideoPressed() async {
    final navigator = Navigator.of(context);
    final xFile = await captureVideo();
    if (xFile != null) {
      if (xFile.path.isNotEmpty) {
        navigator.push(
          MaterialPageRoute(
            builder: (context) => PreviewPage(
              videoPath: xFile.path,
            ),
          ),
        );
      }
    }
  }
```

These methods follow the same logic with the `capturePhoto()` function, where we call the `cameraController?.startVideoRecording()` function and then delay for 5 seconds before calling the `cameraController?.stopVideoRecording()` function, where we then save our 5 second video to an `XFile` and return it.

The `_onRecordVideoPressed()` takes the recorded file and passes to our `PreviewPage` to the `videoPath` parameter.

The below code shows how we are calling the `_onRecordVideoPressed()` method in the UI.

```dart
@override
  Widget build(BuildContext context) {
    if (_isCameraInitialized) {
      return SafeArea(
        child: Scaffold(
          body: Column(
            children: [
	            ....
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
									....
                  ElevatedButton(
                    onPressed:_isRecording? null: _onRecordVideoPressed,
                    style: ElevatedButton.styleFrom(
                        fixedSize: const Size(70, 70),
                        shape: const CircleBorder(),
                        backgroundColor: Colors.white),
                    child: Icon(
                      _isRecording ? Icons.stop : Icons.videocam,
                      color: Colors.red,
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      );
    } else {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }
  }
```

The link below shows a successfully recorded video.

[Video example](https://drive.google.com/file/d/1DaIxYFIEErNvmiyRsrH0SeAbdBj3A1Y1/view?usp=share_link)

### The `PreviewPage` Widget Class

The **`PreviewPage`** widget class is responsible for rendering the captured photos and recorded videos. Let's take a closer look at how this widget handles the display of these media assets.

```dart
class PreviewPage extends StatefulWidget {
  final String? imagePath;
  final String? videoPath;

  const PreviewPage({Key? key, this.imagePath, this.videoPath})
      : super(key: key);

  @override
  State<PreviewPage> createState() => _PreviewPageState();
}

class _PreviewPageState extends State<PreviewPage> {
  VideoPlayerController? controller;

  Future<void> _startVideoPlayer() async {
    if (widget.videoPath != null) {
      controller = VideoPlayerController.file(File(widget.videoPath!));
      await controller!.initialize().then((_) {
        // Ensure the first frame is shown after the video is initialized
        setState(() {});
      });
      await controller!.setLooping(true);
      await controller!.play();
    }
  }

  @override
  void initState() {
    super.initState();
    if (widget.videoPath != null) {
      _startVideoPlayer();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: widget.imagePath != null
            ? Image.file(
                File(widget.imagePath ?? ""),
                fit: BoxFit.cover,
              )
            : AspectRatio(
                aspectRatio: controller!.value.aspectRatio,
                child: VideoPlayer(controller!),
              ),
      ),
    );
  }
}
```

To display images in the **`PreviewPage`** widget, the **`imagePath`** parameter is passed to an **`Image.file`** widget. However, displaying recorded videos requires the use of the **`video_player`**
 package that was added to the **`pubspec.yaml`** file. 

This package provides access to the **`VideoPlayerController`** and **`VideoPlayer`** classes, with the **`VideoPlayerController`** class being responsible for initializing the video file.

The **`_startVideoPlayer()`** method checks if the **`videoPath`** is not null, assigns the **`controller`** to a new **`VideoPlayerController.file(File(widget.videoPath))`**, and initializes the controller. 

Upon successful initialization of the video file, the playback of the video can be initiated through the invocation of the **`controller.play()`** function, and to achieve a continuous playback loop, the **`controller.setlooping(...)`** function must be called with the **`boolean`** value of **`true`** as a parameter.

With all these steps complete, the camera app is fully functional.

## **Conclusion**

In this article, we've explored how to build a camera app with Flutter using the **`camera`** package. We've seen how to initialize the camera, display the camera preview, and capture photos and videos. The **`camera`** package provides a lot of functionality and customization options, making it a great choice for building camera apps with Flutter.

The **`camera`** package provides a lot of other functionality, like adjusting camera settings, setting exposure levels etc, so be sure to check out the [documentation](https://pub.dev/packages/camera) and this [article](https://blog.logrocket.com/flutter-camera-plugin-deep-dive-with-examples) by [Souvik Biswas](https://blog.logrocket.com/author/souvikbiswas/) for more information.

Here is the GitHub [repo](https://github.com/BishopSam/tutorial_articles/tree/main/camera_application) to the full code in this article.