Flutter: Taking Pictures with the Bloc Pattern
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:
The app contains two blocs:
- PhotosBloc, in charge of loading, adding and deleting the photos.
- CameraBloc, in charge of the interactions with the
CameraController
.
We are going to focus on the CameraBloc as it is the purpose of this article.
Bloc Construction
CameraBloc events:
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.
CameraBloc states:
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.
CameraBloc logic:
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.
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…
When the user opens the camera’s screen, the event CameraInitialized
is sent. The camera’s controller is created and initialized.
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.
- If the user denies the permission, the method throws a
CameraException
and the bloc emitsCameraFailure
. - If the user accepts, the bloc emits
CameraReady
.
When the user clicks on the button to capture a photo, the event CameraCaptured
is sent.
The bloc will first emit CameraCaptureInProgress
and if everything goes as expected CameraCaptureSuccess
. Otherwise it will emit CameraCaptureFailure
.
When the user sets the app in background, the event CameraStopped
is sent to release the resources used by the camera and reset the bloc to its initial state. Also, don’t forget to dispose the camera’s controller when closing the bloc.
Camera Screen
I’m using a BlocConsumer
to update the UI on every change of the bloc’s state. I added some keys to the widgets for the widget tests where we will verify that the UI looks as expected.
Unit Tests
Now comes the most interesting part, the tests !
Bloc Tests
For the bloc tests, I’m only going to detail the tests for the CameraInitialized
event. You can find all the tests available in the source code.
As explained at the beginning we need to simulate the camera’s controller to see how the bloc behaves. To do so we are going to mock the CameraController
and the CameraUtils
classes.
Nothing really fancy here, we are just initializing the bloc and defining the default response of the methods.
- Emits [CameraFailure] when permission is not granted.
In this test we are simulating a CameraExeption
thrown by the initialization of the cameraController
. In response to this exception the bloc should emit CameraFailure
with the error message.
- Emits [CameraFailure] when there is no camera on the device
Now we are simulating an exception when trying to select a camera of the device ( ex: no camera available). Once again, the bloc should emit CameraFailure
with the error message.
- Emits [CameraReady] when permission is granted.
Finally we are simulating the correct behavior: permission granted and no problem when initializing the camera. This time the bloc should emit CameraReady
.
Widget Tests
Thanks to the CameraBloc we can easily test the camera’s screen and be sure about what is being displayed.
It’s all for me, I hope that this post can help some people developing their bloc and building fully testable Flutter’s apps.
PS: I’m just a simple dev trying to share what he learned so if there are some mistakes (sorry…) or if it can be improved you are welcome to contact me !