Using the Camera on Flutter
See how to integrate the device camera into your Flutter app for iOS and Android. By Sagar Suri.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Using the Camera on Flutter
15 mins
In today’s world, mobile cameras are the new definition of communication and entertainment. People use the camera to capture still photos or videos to share with friends and family, upload selfies or videos to social network platforms, make video calls, and more.
When it comes to mobile, almost every app out there uses the camera to perform various tasks, and if you are reading this tutorial then maybe you have a multi-million dollar app idea which depends on the camera or you want to learn how to integrate camera in a Flutter app.
In this tutorial, you’ll be building a Flutter app which will use the device’s camera (on Android or iOS) to display a preview, take a photo, and share it with your friends.
Introduction
Before you start building the app, let me give you a brief description of what it feels like to access the camera in Android or iOS natively.
On Android, if you want to access the device camera you need to create a SurfaceView to render previews of what the camera sensor is picking up. This requires a deeper understanding of the SurfaceView
to properly control the camera hardware. On iOS, you need to know about the AVFoundation Capture subsystem that provides a high-level architecture to build a custom camera UI.
If you are just getting started with mobile app development, then it will be quite difficult to understand both those APIs. Not only that, you also need to know both the Kotlin and Swift programming languages to write separate apps for Android and iOS.
But with Flutter, you don’t have to worry about all that: With one codebase you can build an app that can accesses the camera hardware on both Android and iOS. Isn’t that amazing? :]
So in this tutorial, you will make an app that will open up the camera preview on launch. You can switch between the front and back camera, click a picture, and share it with your friends. While building this app you will learn how to use the camera package built by the Flutter team, save the image to a path, and share the image to different social platforms.
Getting Started
Download the starter project using the Download Materials at the top or bottom of the tutorial. The starter project contains the required libraries and some boilerplate code. The basic UI for this app is already built so that you can focus on how to integrate the device camera.
Fire up Android Studio 3.4 or later with the Flutter plugin installed, and choose the option Open an existing Android Studio project. Select the starter project which you just downloaded. Image for reference:
After opening the project, click Get dependencies on the Packages get message near the top of Android Studio, to pull down the project dependencies.
Before you make any changes to the starter project, run it to see the current state of the app. If you press the green Run button in Android Studio, you should be able to run the app and see the following screen on your mobile phone, iOS Simulator, or Android emulator:
Exploring the Project
Once you have run the starter project it’s time to take a look at the project structure, expand the lib folder and check the folders within it. It should look like this:
camera_screen.dart is the camera preview screen where you can click a picture and toggle between front or back camera, preview_screen.dart is the screen where you will see the preview of the image you clicked and will have the option to share that image with your friends. Finally, main.dart is the root widget of your app.
Open the pubspec.yaml file to see all the dependencies required for the app. It should look like this:
As you can see, beyond the usual dependencies, there are four libraries that have added to the project:
- camera: A Flutter plugin for iOS and Android allowing access to the device cameras.
- path_provider: A Flutter plugin for finding commonly used locations on the filesystem. Supports both iOS and Android.
- path: A comprehensive, cross-platform path manipulation library for Dart.
- esys_flutter_share: A Flutter plugin for sharing files and text with other applications.
Coding the Camera Screen
It’s time to look at all the Dart files one by one.
Open the main.dart file:
import 'package:flutter/material.dart';
import 'camerascreen/camera_screen.dart';
class CameraApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CameraScreen(),
);
}
}
void main() => runApp(CameraApp());
CameraApp
is the root widget. This is the entry point for your app. Its responsibility is to launch the CameraScreen
widget where you will see the camera preview.
Next, open the camera_screen.dart file. This is a StatefulWidget
, because you will be adding the feature to toggle between the front and back camera (changing the state of the camera). If you go through the file you will find some TODO’s which you will be addressing soon.
Going step by step, first add four properties to the _CameraScreenState
class as shown below:
class _CameraScreenState extends State {
CameraController controller;
List cameras;
int selectedCameraIdx;
String imagePath;
You might see a red squiggly error line under CameraController
. If not, that’s because the needed import is already in the starter project file. But if so, you need to import the camera
package to make the error go away. Click on the line and press option+return on macOS or Alt+Enter on PC. You will see a menu popup:
Choose the import option to add the necessary import.
You must be curious to know about these four properties you just added:
-
CameraController controller
: This class is responsible for establishing a connection to the device’s camera. -
List cameras
: This will hold a list of the cameras available on the device. Normally, the size of the list will be 2 i.e. a front and back camera. The0
index is for the back camera and the1
index for the front camera. -
selectedCameraIdx
: This will hold the current camera index that the user has selected. -
imagePath
: This will hold the path of the image which you will create using the camera.
Next, override the initState()
method and add the following code to it:
@override void initState() { super.initState(); // 1 availableCameras().then((availableCameras) { cameras = availableCameras; if (cameras.length > 0) { setState(() { // 2 selectedCameraIdx = 0; }); _initCameraController(cameras[selectedCameraIdx]).then((void v) {}); }else{ print("No camera available"); } }).catchError((err) { // 3 print('Error: $err.code\nError Message: $err.message'); }); }
initState()
is one of the lifecycle methods of a StatefulWidget
. It will be called when CameraScreen
is inserted into the widget tree.
In this code:
-
availableCameras()
is part of thecamera
plugin which will return a list of available cameras on the device. - Initially,
selectedCameraIdx
will be0
, as you will be loading the back camera at every cold launch of the app. You can change the value from0
to1
if the front camera is your initial preference. - In the process of getting the list of cameras if something goes wrong, the code execution will enter the
catchError()
function.
Now, create the missing method _initCameraController
and add the following code to it:
// 1, 2
Future _initCameraController(CameraDescription cameraDescription) async {
if (controller != null) {
await controller.dispose();
}
// 3
controller = CameraController(cameraDescription, ResolutionPreset.high);
// If the controller is updated then update the UI.
// 4
controller.addListener(() {
// 5
if (mounted) {
setState(() {});
}
if (controller.value.hasError) {
print('Camera error ${controller.value.errorDescription}');
}
});
// 6
try {
await controller.initialize();
} on CameraException catch (e) {
_showCameraException(e);
}
if (mounted) {
setState(() {});
}
}
Here is a step by step explanation:
-
_initCameraController
is responsible for initializing theCameraController
object. Initializing aCameraController
object is asynchronous work. Hence the return type of this method is aFuture
. -
CameraDescription
will hold the type of camera(front or back) you want to use. - You are creating a
CameraController
object which takes two arguments, first acameraDescription
and second aresolutionPreset
with which the picture should be captured.ResolutionPreset
can have only 3 values i.ehigh
,medium
andlow
. -
addListener()
will be called when the controller object is changed. For example, this closure will be called when you switch between the front and back camera. -
mounted
is a getter method which will return a boolean value indicating whether theCameraScreenState
object is currently in the widget tree or not. - While initializing the
controller
object if something goes wrong you will catch the error in atry/catch
block.
Now you’ll complete the _cameraPreviewWidget()
implementation. Replace the TODO and Container
widget with the following code:
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),
);
}
This will return a CameraPreview
widget if the controller object is initialized successfully, or else a Text
widget with the label ‘Loading’. The CameraPreview
widget will return a camera view.
Displaying the Camera Preview
From this point you’ll need to be on a physical device. Run the project and you should see the following screen:
Amazing! You’ve just integrated the camera feature into your app!
But as you can see, there is no way to switch between the front and back camera, and if you press the capture button nothing happens. You’ll implement those features next.
Switching Cameras
Update _cameraTogglesRowWidget()
to look as follows:
Widget _cameraTogglesRowWidget() {
if (cameras == null || cameras.isEmpty) {
return Spacer();
}
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)}")),
),
);
}
In the above code, you are just showing an icon to the user which indicates the currently selected camera (front or back). On pressing the FlatButton
you can toggle between front and back camera.
Now add the _getCameraLensIcon()
method:
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;
}
}
_getCameraLensIcon()
just returns a specific icon based on the camera selected.
Next, add the logic to toggle between the front and back camera. Create the method _onSwitchCamera()
:
void _onSwitchCamera() {
selectedCameraIdx =
selectedCameraIdx < cameras.length - 1 ? selectedCameraIdx + 1 : 0;
CameraDescription selectedCamera = cameras[selectedCameraIdx];
_initCameraController(selectedCamera);
}
In the above code, you are just changing the index, i.e. 0
for the back camera and 1
for the front camera. You are creating a new CameraDescription
which will hold the camera object based on the updated index and then again initialize the CameraController
object by calling _initCameraController()
.
Build and run the app to your device. Tap the icon at the bottom-left corner of the screen to toggle between the front and back camera. The screens should look as follows:
Taking a Picture
It’s time you start taking some awesome images using this camera app you’ve built!
Update the _onCapturePressed()
to look as follows:
void _onCapturePressed(context) async {
try {
// 1
final path = join(
(await getTemporaryDirectory()).path,
'${DateTime.now()}.png',
);
// 2
await controller.takePicture(path);
// 3
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PreviewImageScreen(imagePath: path),
),
);
} catch (e) {
print(e);
}
}
You’ll see some errors at this point. You need to import the appropriate packages to make the errors go away. Click on each red line and use option+return or Alt+Enter to import the packages.
The above code step-by-step is as follows:
-
join(..)
will take different paths and join them to make a final path. For example:p.join('path', 'to', 'foo'); // -> 'path/to/foo'
-
takePicture(path)
will capture a raw image and write it to the path provided as an argument. - After the picture is saved successfully to the specified path, you navigate to
PreviewImageScreen
to preview the image and share it with your friends.
Run the app and you will be able to take a picture, which will navigate to the PreviewScreen
:
Sharing the Image
If you click on the Share Button
you will be presented with a Bottomsheet Dialog
through which you can share the image you have just clicked.
Open the preview_screen.dart file in order to see the code behind the share button.
The getBytesFromFile()
method converts the image file into ByteData
:
Future<ByteData> getBytesFromFile() async {
Uint8List bytes = File(widget.imagePath).readAsBytesSync() as Uint8List;
return ByteData.view(bytes.buffer);
}
The onPressed
handler for the share button calls getBytesFromFile()
.
onPressed: () {
getBytesFromFile().then((bytes) {
Share.file('Share via:', basename(widget.imagePath),
bytes.buffer.asUint8List(), 'image/png');
});
},
After that call returns, the bytes from the file are passed to the file()
method of Share
class. The Share
class is a part of esys_flutter_share
plugin which you already added in the starter project.
You’ve successfully integrated the camera, and it’s time to play around with the app you just created. Take some amazing pictures and share them with your friends to demonstrate your photography skills!
Where to Go From Here?
Check out the final completed project by clicking the Download Materials button at the top of bottom of the tutorial.
You can read more about using a camera with Flutter in the official Flutter documentation.
You can explore more about the various plugins we just used in this tutorial:
Try adding them to other Flutter projects by yourself.
I hope you enjoyed this tutorial on integrating the camera into your Flutter app! Feel free to share your feedback, findings or ask any questions in the comments below or in the forums.