self-dslr

Introduction

Working in Flutter to build my new mobile app (now defunct) ZapCam has been an amazing experience.

For those who do not know, Flutter is an open-sourced cross-platform mobile app development framework, developed by Google and written in Dart. Developing in it has been surprisingly elegant. The type system of Dart is clean and expressive, and Flutter itself offers many development tools that come prepackaged that makes development, building and deploying for device a total breeze.

One of the aspects of Flutter that I spent a long time studying was how to work with the recommended camera plugin on pub.dev (v0.5.8+2), which ZapCam relies heavily on (because ZapCam is a camera application!). Today I want to go into as much detail as I can on how to build out a camera widget in Flutter, covering key functionality like setup, toggling and disabling.

In the process of this, the hope is that you will walk away from this understanding how the different parts of the camera API interact together. This will go a long way as well to leveraging other camera plugins in Flutter, because most of these other plugins are forks of this plugin — and usually act as drop-in replacements, for example in the case of flutter better camera.

All the code here can be found on Github.

1. Example Code

Assuming that Flutter has already been installed, we create 3 files: main.dart, gallery.dart and camera.dart.

Main

This file should be familiar to anyone who has used Flutter before. Here we simply setup a MaterialApp widget, and define 2 routes: Camera and Gallery.

The important part here is this:

The call to WidgetsFlutterBinding.ensureInitialized() before runApp() is required for the camera plugin to work.

Cameras usually come with a gallery, so let’s include some filler code for that.

Camera

The meat of the example. This is where the camera will be initialised and manipulated.

Notice that we initialise the camera widget as a StatefulWidget. This is because in order for the widget to function, we need to retain references to 2 critical pieces of state: List, and CameraController. We will explain what these 2 are in the next section.

These file are not intended to be usable as is — you will have to add in the triggers for the individual features (like taking a picture, or toggling the camera) on your own depending on your use case. These will most likely take the form of callbacks to touch events.

2. Initialisation

Initialising the camera requires 3 steps:

  1. Asking for permissions,
  2. Getting the list of available cameras, then
  3. Selecting the camera we want to use.

The terminology can get a little confusing here. Let me explain why.

When we await availableCameras(), we do not actually get a list of cameras, but rather a list of camera descriptions — List<CameraDescription> from above. Each camera description refers to information related to each camera on the device. Usually this consists of the front and back cameras.

We then take this list of camera descriptions, and select the one that we want. Because we are doing this for the first time, we simply default to index 0, as represented by the initialised value of _selected. This description we pass into CameraController — the second important piece of state from above. We then proceed to initialise it.

This instance that we get from CameraController represents the “camera” per se. Interacting with this instance is what we will use to control our camera. We will use _controller to stream our camera feed, take pictures, as well as all the other things a camera is supposed to do.

Notice that after we get the controller we call setState() with it. This triggers a re-render with the new _controller.

3. Getting the Camera Feed

Displaying the current camera feed is as simple as passing the _controller (that we updated with setState) into CameraPreview. This creates a widget that streams the camera feed and makes it visible to the user.

It is worth understanding how this works, if only out of curiosity. Remember that Flutter uses widgets to display UI components to the user. So it makes sense that in order to allow the user to view the camera feed, we have to build and return a widget at some point in the application.

However, UI and State are separate beasts, and Flutter takes great pains to separate the two. Also, the camera controller is the only piece of state right now that is aware of and able to communicate with the selected camera.

Therefore the delegation of responsibility is as follows: while CameraPreview handles the how of displaying a stream of data from the camera feed, CameraController handles how to acquire and transfer that stream of data from a specific camera.

Together they allow what the camera sees to be displayed to the user.

4. Taking a Picture

Taking picture is as simple as calling _controller.takePicture(path), where path is an argument to the location on the local machine the picture should be saved to. I have included some setup code to easily generate an available path.

It should become clearer and clearer now that the camera controller is pretty important.

5. Toggling

Remember how we saved the list of camera descriptions — List<CameraDescription> — to a state variable. We can use it again to switch the camera we want to use.

There are many ways to select the new camera, including matching by the camera’s name property, but for this use case we will simply cycle through the list of camera descriptions. We then select the camera according to the newly selected camera description.

6. Disabling

This was admittedly one of the biggest pitfalls I faced while working with flutter camera.

The problem I experienced involved the fact that the camera plugin on pub.dev (v0.5.8+2) recommended by the Flutter docs does not automatically switch off the camera when the camera preview screen is out of focus. This mean that the camera remains on even when we switch over to a different screen in the app — and worse even when we put the app in the background. In both cases, the light indicating the camera is still streaming continues to be lit!

As a simple example, let’s assume that we are viewing the camera feed currently through a CameraPreview widget. Then we execute the following code:

This simple line in the application brings us to the gallery screen to view all the pictures that have been taken. But while here — unless we handle this in our code — the camera continues to stream in the background, leaving the indicator light on.

This was a great source of confusion for me personally, because it seems clear that once the camera goes out of focus, we would want the camera to stop streaming so as not to freak out the app’s users. Especially in this age of heightened privacy concerns. Regardless this is a decision that seems to have been made by the dev team, perhaps as a way to increase developer control over the camera.

The solution to this is problem relies on listening to the app’s lifecycle state:

A couple of things are happening here. Let’s start at the top.

First we have to introduce the WidgetsBindingObserver mixin, which will allow the camera widget to respond to the app’s lifecycle events. We then set up the WidgetBindings to observe on said events, making sure that we remove the observer just before we dispose of the component.

Now our application is able to listen to changes in the app’s lifecycle.

Then we override didChangeAppLifecycleState, disposing of the camera controller when AppLifecycleState is inactive, and setting it back up again when AppLifecycleState is resumed.

7. Conclusions

This concludes my examination about how to use the camera in Flutter.

I hope this bit of writing has been useful in helping you develop a better appreciation of several important concepts when it comes to working with the camera in Flutter.

Once again, feel free to check out the code on Github if you need some code to reference.