Working with flutter makes it really easy to build applications using the camera. Just by following the small example of the camera_plugin we can successfully take pictures with minimum lines of code.
However when the moment comes to cover the camera with tests, things get harder. In this article we are going to see how we can use Bloc Design Pattern to take photos, making our app more robust and easily testable.
For the purpose of this article, I created a simple app named MyPhotos (available on GitHub) using Bloc to take some pictures and show them on the home page:
MyPhotos Demo
The app contains two blocs:
CameraController
.We are going to focus on the CameraBloc as it is the purpose of this article.
We only need 3 events to manage the camera.
CameraInitialized
to start the camera (ask for permission and stream the camera).CameraStopped
to release the resources used by the camera.CameraCaptured
to take a picture.Depending on the events the bloc can emit 6 states:
CameraInitial
for the initial state.CameraReady
when the camera is ready to take a picture.CameraFailure
when the camera failed to initialize.CameraCaptureInProgress
when the camera is taking a picture.CameraCaptureSuccess
when the camera successfully took a picture.CameraCaptureFailure
when an error happened during the capture.To be able to mock the camera’s controller during the tests, we need to pass it through dependency injection. We could directly pass an instance of CameraController
through the constructor, but I prefer to use a class with a method in charge to instantiate the controller to be able to catch any error inside of the bloc.
class CameraUtils {
Future<CameraController> getCameraController(
ResolutionPreset resolutionPreset,
CameraLensDirection cameraLensDirection) async {
final cameras = await availableCameras();
final camera = cameras
.firstWhere((camera) => camera.lensDirection == cameraLensDirection);
return CameraController(camera, resolutionPreset, enableAudio: false);
}
Future<String> getPath() async => join(
(await getTemporaryDirectory()).path,
'${DateTime.now()}.png',
);
}
By passing an object of CameraUtils
we will then be able to mock all of its methods. In a more complex example we could add methods to compress, crop…
class CameraBloc extends Bloc<CameraEvent, CameraState> {
final CameraUtils cameraUtils;
final ResolutionPreset resolutionPreset;
final CameraLensDirection cameraLensDirection;
CameraController _controller;
CameraBloc({
@required this.cameraUtils,
this.resolutionPreset = ResolutionPreset.high,
this.cameraLensDirection = CameraLensDirection.back,
}) : super(CameraInitial());
// ...
}
When the user opens the camera’s screen, the event CameraInitialized
is sent. The camera’s controller is created and initialized.
Stream<CameraState> _mapCameraInitializedToState(CameraInitialized event) async* {
try {
_controller = await cameraUtils
.getCameraController(resolutionPreset, cameraLensDirection);
await _controller.initialize();
yield CameraReady();
} on CameraException catch (error) {
_controller?.dispose();
yield CameraFailure(error: error.description);
} catch (error) {
yield CameraFailure(error: error.toString());
}
}
If an error occurred when selecting a camera (or if there is no camera) the second catch will handle the exception and emits CameraFailure
.
By calling _controller.initialize()
the plugin automatically checks if the permissions are granted and if not, opens a pop-up to ask for the permissions.
CameraException
and the bloc emits CameraFailure
.CameraReady
.#flutter #mobile-apps #programming #developer #web-development