Flutter - Access Camera and Capture Image Example

Are you developing a Flutter application which needs to access the camera for taking picture? In this tutorial, I'm going to show you how to capture image by accessing camera, either rear-facing, front-facing or even external camera. In the end, the simple camera application should look like this.

Flutter Camera Application Screenshot

Dependencies

There are two dependencies we're going to use. The first one is camera, which is used to handle everything related to camera, including accessing the camera and requesting permission. The other plugin is fluttertoast, which is used for displaying message in a toast. Add the following in your pubspec.yml file.

  dependencies:
    camera: ^0.2.9+1
    fluttertoast: ^2.2.6

Code

Below is the code structure of the simple camera application. Later, we will add many things inside _CameraExampleState

  class CameraExample extends StatefulWidget {
   @override
    _CameraExampleState createState() {
      return _CameraExampleState();
    }
  }

  class _CameraExampleState extends State {
    CameraController controller;
    List cameras;
    int selectedCameraIdx;
    String imagePath;

    final GlobalKey _scaffoldKey = GlobalKey();

    @override
    void initState() {
      super.initState();
    }
  }

  class CameraApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        home: CameraExample(),
      );
    }
  }

  Future main() async {
    runApp(CameraApp());
  }

First, we need to get the list of available cameras. Most devices have two cameras, one on the back and another one on the front. But, it's also possible to add an additional external camera. In this example, the first camera will be loaded at the start. Later, user can switch between cameras by pressing a camera switch button.

The camera library has availableCameras() which returns a Future<List<CameraDescription>>. The app needs to store current camera index, then load the first camera. The code for handling camera switching is inside_onCameraSwitched. Because we want to load the first camera at the start, we also need to call it.

@override
  void initState() {
    super.initState();

    // Get the list of available cameras.
    // Then set the first camera as selected.
    availableCameras()
    .then((availableCameras) {
      cameras = availableCameras;

      if (cameras.length > 0) {
        setState(() {
          selectedCameraIdx = 0;
        });

        _onCameraSwitched(cameras[selectedCameraIdx]).then((void v) {});
      }
    })
    .catchError((err) {
      print('Error: $err.code\nError Message: $err.message');
    });
  }

Below is the code that will be called every time user switch the cameras. We need to dispose the camera controller if it's not null, then load a new CameraController and add a listener to it. Inside, we call setState so that the view will be refreshed. If something error, just show the error on a toast.

In addition, we also need to handle when user clicking on switch camera button. Simply change the selectedCameraIdx to the next camera, call _onCameraSwitched and update the state.

  Future _onCameraSwitched(CameraDescription cameraDescription) async {
    if (controller != null) {
      await controller.dispose();
    }

    controller = CameraController(cameraDescription, ResolutionPreset.high);

    // If the controller is updated then update the UI.
    controller.addListener(() {
      if (mounted) {
        setState(() {});
      }

      if (controller.value.hasError) {
        Fluttertoast.showToast(
            msg: 'Camera error ${controller.value.errorDescription}',
            toastLength: Toast.LENGTH_SHORT,
            gravity: ToastGravity.CENTER,
            timeInSecForIos: 1,
            backgroundColor: Colors.red,
            textColor: Colors.white
        );
      }
    });

    try {
      await controller.initialize();
    } on CameraException catch (e) {
      _showCameraException(e);
    }

    if (mounted) {
      setState(() {});
    }
  }

  void _onSwitchCamera() {
    selectedCameraIdx = selectedCameraIdx < cameras.length - 1
        ? selectedCameraIdx + 1
        : 0;
    CameraDescription selectedCamera = cameras[selectedCameraIdx];

    _onCameraSwitched(selectedCamera);

    setState(() {
      selectedCameraIdx = selectedCameraIdx;
    });
  }

Next, we need to handle when user press the capture button. The _takePicture function is used to handle image capture by calling CameraController.takePicture. Beforehand, it checks whether the controller has been initialized and there's no taking picture process in progress. In this tutorial, the picture is always stored in the Pictures directory of the application's directory. If you want to use another directory, you can read our tutorial about writing to storage using Flutter, jump to Determining the Directory Path section

When a user click on the capture button, it will be handled inside _onCapturePressed() which calls _takePicture. If success, a toast containing the image path will be shown.

  Future _takePicture() async {
    if (!controller.value.isInitialized) {
      Fluttertoast.showToast(
          msg: 'Please wait',
          toastLength: Toast.LENGTH_SHORT,
          gravity: ToastGravity.CENTER,
          timeInSecForIos: 1,
          backgroundColor: Colors.grey,
          textColor: Colors.white
      );

      return null;
    }

    // Do nothing if a capture is on progress
    if (controller.value.isTakingPicture) {
      return null;
    }

    final Directory appDirectory = await getApplicationDocumentsDirectory();
    final String pictureDirectory = '${appDirectory.path}/Pictures';
    await Directory(pictureDirectory).create(recursive: true);
    final String currentTime = DateTime.now().millisecondsSinceEpoch.toString();
    final String filePath = '$pictureDirectory/${currentTime}.jpg';

    try {
      await controller.takePicture(filePath);
    } on CameraException catch (e) {
      _showCameraException(e);
      return null;
    }

    return filePath;
  }

  void _onCapturePressed() {
    _takePicture().then((String filePath) {
      if (mounted) {
        setState(() {
          imagePath = filePath;
        });

        if (filePath != null) {
          Fluttertoast.showToast(
              msg: 'Picture saved to $filePath',
              toastLength: Toast.LENGTH_SHORT,
              gravity: ToastGravity.CENTER,
              timeInSecForIos: 1,
              backgroundColor: Colors.grey,
              textColor: Colors.white
          );
        }
      }
    });
  }

Now let's implement the build method which is used to render the layout. As you can see on the example, the layout is very simple. It consists mainly of a camera preview widget which fills a large portion of the screen. On the bottom, there is a camera switch button, a capture button, and a place for the thumbnail of last captured photo. Below is the full code which includes the code for rendering layout.

  import 'dart:async';
  import 'dart:io';
  
  import 'package:camera/camera.dart';
  import 'package:flutter/material.dart';
  import 'package:fluttertoast/fluttertoast.dart';
  import 'package:path_provider/path_provider.dart';
  
  class CameraExample extends StatefulWidget {
    @override
    _CameraExampleState createState() {
      return _CameraExampleState();
    }
  }
  
  class _CameraExampleState extends State {
    CameraController controller;
    List cameras;
    int selectedCameraIdx;
    String imagePath;
  
    final GlobalKey _scaffoldKey = GlobalKey();
  
    @override
    void initState() {
      super.initState();
  
      // Get the list of available cameras.
      // Then set the first camera as selected.
      availableCameras()
      .then((availableCameras) {
        cameras = availableCameras;
  
        if (cameras.length > 0) {
          setState(() {
            selectedCameraIdx = 0;
          });
  
          _onCameraSwitched(cameras[selectedCameraIdx]).then((void v) {});
        }
      })
      .catchError((err) {
        print('Error: $err.code\nError Message: $err.message');
      });
    }
  
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        key: _scaffoldKey,
        appBar: AppBar(
          title: const Text('Camera Example'),
        ),
        body: Column(
          children: [
            Expanded(
              child: Container(
                child: Padding(
                  padding: const EdgeInsets.all(1.0),
                  child: Center(
                    child: _cameraPreviewWidget(),
                  ),
                ),
                decoration: BoxDecoration(
                  color: Colors.black,
                  border: Border.all(
                    color: Colors.grey,
                    width: 3.0,
                  ),
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(5.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.start,
                children: [
                  _cameraTogglesRowWidget(),
                  _captureControlRowWidget(),
                  _thumbnailWidget(),
                ],
              ),
            ),
          ],
        ),
      );
    }
  
    /// Display 'Loading' text when the camera is still loading.
    Widget _cameraPreviewWidget() {
      if (controller == null || !controller.value.isInitialized) {
        return const Text(
          'Loading',
          style: TextStyle(
            color: Colors.white,
            fontSize: 20.0,
            fontWeight: FontWeight.w900,
          ),
        );
      }
  
      return AspectRatio(
        aspectRatio: controller.value.aspectRatio,
        child: CameraPreview(controller),
      );
    }
  
    /// Display the thumbnail of the captured image
    Widget _thumbnailWidget() {
      return Expanded(
        child: Align(
          alignment: Alignment.centerRight,
          child: imagePath == null
            ? SizedBox()
            : SizedBox(
              child: Image.file(File(imagePath)),
              width: 64.0,
              height: 64.0,
            ),
        ),
      );
    }
  
    /// Display the control bar with buttons to take pictures
    Widget _captureControlRowWidget() {
      return Expanded(
        child: Align(
          alignment: Alignment.center,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            mainAxisSize: MainAxisSize.max,
            children: [
              IconButton(
                icon: const Icon(Icons.camera_alt),
                color: Colors.blue,
                onPressed: controller != null &&
                    controller.value.isInitialized
                    ? _onCapturePressed
                    : null,
              )
            ],
          ),
        ),
      );
    }
  
    /// Display a row of toggle to select the camera (or a message if no camera is available).
    Widget _cameraTogglesRowWidget() {
      if (cameras == null) {
        return Row();
      }
  
      CameraDescription selectedCamera = cameras[selectedCameraIdx];
      CameraLensDirection lensDirection = selectedCamera.lensDirection;
  
      return Expanded(
        child: Align(
          alignment: Alignment.centerLeft,
          child: FlatButton.icon(
              onPressed: _onSwitchCamera,
              icon: Icon(
                  _getCameraLensIcon(lensDirection)
              ),
              label: Text("${lensDirection.toString()
                  .substring(lensDirection.toString().indexOf('.')+1)}")
          ),
        ),
      );
    }
  
    IconData _getCameraLensIcon(CameraLensDirection direction) {
      switch (direction) {
        case CameraLensDirection.back:
          return Icons.camera_rear;
        case CameraLensDirection.front:
          return Icons.camera_front;
        case CameraLensDirection.external:
          return Icons.camera;
        default:
          return Icons.device_unknown;
      }
    }
  
    Future _onCameraSwitched(CameraDescription cameraDescription) async {
      if (controller != null) {
        await controller.dispose();
      }
  
      controller = CameraController(cameraDescription, ResolutionPreset.high);
  
      // If the controller is updated then update the UI.
      controller.addListener(() {
        if (mounted) {
          setState(() {});
        }
  
        if (controller.value.hasError) {
          Fluttertoast.showToast(
              msg: 'Camera error ${controller.value.errorDescription}',
              toastLength: Toast.LENGTH_SHORT,
              gravity: ToastGravity.CENTER,
              timeInSecForIos: 1,
              backgroundColor: Colors.red,
              textColor: Colors.white
          );
        }
      });
  
      try {
        await controller.initialize();
      } on CameraException catch (e) {
        _showCameraException(e);
      }
  
      if (mounted) {
        setState(() {});
      }
    }
  
    void _onSwitchCamera() {
      selectedCameraIdx = selectedCameraIdx < cameras.length - 1
          ? selectedCameraIdx + 1
          : 0;
      CameraDescription selectedCamera = cameras[selectedCameraIdx];
  
      _onCameraSwitched(selectedCamera);
  
      setState(() {
        selectedCameraIdx = selectedCameraIdx;
      });
    }
  
    Future _takePicture() async {
      if (!controller.value.isInitialized) {
        Fluttertoast.showToast(
            msg: 'Please wait',
            toastLength: Toast.LENGTH_SHORT,
            gravity: ToastGravity.CENTER,
            timeInSecForIos: 1,
            backgroundColor: Colors.grey,
            textColor: Colors.white
        );
  
        return null;
      }
  
      // Do nothing if a capture is on progress
      if (controller.value.isTakingPicture) {
        return null;
      }
  
      final Directory appDirectory = await getApplicationDocumentsDirectory();
      final String pictureDirectory = '${appDirectory.path}/Pictures';
      await Directory(pictureDirectory).create(recursive: true);
      final String currentTime = DateTime.now().millisecondsSinceEpoch.toString();
      final String filePath = '$pictureDirectory/${currentTime}.jpg';
  
      try {
        await controller.takePicture(filePath);
      } on CameraException catch (e) {
        _showCameraException(e);
        return null;
      }
  
      return filePath;
    }
  
    void _onCapturePressed() {
      _takePicture().then((String filePath) {
        if (mounted) {
          setState(() {
            imagePath = filePath;
          });
  
          if (filePath != null) {
            Fluttertoast.showToast(
                msg: 'Picture saved to $filePath',
                toastLength: Toast.LENGTH_SHORT,
                gravity: ToastGravity.CENTER,
                timeInSecForIos: 1,
                backgroundColor: Colors.grey,
                textColor: Colors.white
            );
          }
        }
      });
    }
  
    void _showCameraException(CameraException e) {
      String errorText = 'Error: ${e.code}\nError Message: ${e.description}';
      print(errorText);
  
      Fluttertoast.showToast(
          msg: 'Error: ${e.code}\n${e.description}',
          toastLength: Toast.LENGTH_SHORT,
          gravity: ToastGravity.CENTER,
          timeInSecForIos: 1,
          backgroundColor: Colors.red,
          textColor: Colors.white
      );
    }
  }
  
  class CameraApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        home: CameraExample(),
      );
    }
  }
  
  Future main() async {
    runApp(CameraApp());
  }
  

Now you can try to run the code on your device. After a picture successfully captured, you should see a thumbnail on the bottom right. We also have a tutorial about how to record video using Flutter.