I Implemented the Flutter Camera package With Bloc. Here's How.

I Implemented the Flutter Camera package With Bloc. Here's How.

In this article, we are going to use the Flutter camera package alongside BloC to access the phone camera, take a picture with it and implement some basic camera settings like resolution and switching to the front camera.

Exciting! Let's begin.

Getting Started

Steps we will be taking:

  • Create a new Flutter project.
  • Add dependencies
  • Create the App states
  • Create the Cubit and it's methods
  • Build the App screens
  • Write the routing logic
  • Test and voila!


Create a new Flutter project.

Open Android Studio and find the option to create a new Flutter project. Set the name of your project and other basic settings.


Add dependencies.

We will need four dependencies. Run the following command in your terminal to add the dependencies we are going to need.

 flutter pub add flutter_bloc camera equatable bloc        

Create the App states.

Let us get to more exciting things, shall we?

When we open our app we first arrive at the screen where we can select a profile picture for a potential user. Let's call this state SelectProfilePhotoState{}.

When we click on the camera button, the camera screen opens. From here we can take a photo. Let's call this state CameraState{}.

We click the button to take a photo, on successfully taking the photo we are moved to a preview screen showing the photo. Here we can either approve the photo or go back. Let's call this state the PreviewState{}.

If we approve the photo we are taken back to the SelectProfilePhotoState{} with the selected photo as the profile picture.

To create these states we first need to create an abstract class which our states will depend on:

Create a folder called bloc and inside it add a file, photo_app_state.dart. Then add this code:

abstract class PhotoAppState extends Equatable {
  final bool? isLoading;
  final bool? hasError;
  final String? errorMessage;

  const PhotoAppState({
    this.errorMessage,
    this.isLoading,
    this.hasError
  });
}
        

This class will have three variables so that every state class that extends will also share these variables. It will also extend Equatable from the equatable package so we can compare two state classes.

Let's create the states. Add these to the photo_app_state.dart file we created earlier.

 class InitialState extends PhotoAppState{
  const InitialState();

  @override
  List<Object?> get props => [];
}


class SelectProfilePhotoState extends PhotoAppState {
  final File? file;

  const SelectProfilePhotoState({
    this.file,
    super.isLoading,
    super.errorMessage,
    super.hasError,
  });

  @override
  List<Object?> get props =>[file, isLoading, errorMessage, hasError];
 }

class CameraState extends PhotoAppState {
  final CameraDescription camera;
  final CameraController controller;

  const CameraState({
    required this.controller,
    required this.camera,
    super.isLoading,
    super.errorMessage,
    super.hasError,
  });

  @override
  List<Object?> get props => [controller, camera, isLoading, errorMessage, hasError];
}

class PreviewState extends PhotoAppState {
  final File? file;

  const PreviewState({
    required this.file,
    super.isLoading,
    super.errorMessage,
    super.hasError
  });

  @override
  List<Object?> get props => [file, isLoading, errorMessage, hasError];
}        

Each of this class is a potential state our app can be in. Observe how the individual states carry their own variables alongside the variables from the super class. We will work with these variables as you will soon see.

Create the Cubit and it's methods

Add another file to the bloc folder, photo_app_cubit.

Our Cubit will emit states that will cause our UI to react. It will handle the logic of our app so that our UI can be uncluttered. Let us create the Cubit and add the methods that will emit the states.

Here is a snapshot of our Cubit with the SelectProfilePhotoState{} as it's initial state. We will need four methods as outlined below..

 class PhotoAppCubit extends Cubit<PhotoAppState> {
  PhotoAppCubit() : super(const SelectProfilePhotoState()); <== //initial state
  
  //a method to open the camera 
  //a method to take photo
  //a method to approve the photo
  //a method to switch the camera settings
}        


Let's tackle the first method. Opening the camera.

We will now work with the Flutter camera package. Make sure it is imported, or import it like so:

import 'package:camera/camera.dart';        


Here is what the method to open the camera will look like:

openCamera() async {
    //get available cameras
    final cameras = await availableCameras();

    //get a camera controller
    final cameraController = CameraController(
        cameras.first,
        ResolutionPreset.high,
        imageFormatGroup: ImageFormatGroup.jpeg
    );

    //initialize camera
    await cameraController.initialize();
    emit (CameraState(
        controller: cameraController,
        camera: cameraController.description
    ));
  }        

First, we will grab a hold of the available cameras on the mobile device like so:

final cameras = await availableCameras();        

Then we will need a camera controller so we can use this controller to manipulate our gotten cameras. Do this:

 final cameraController = CameraController(
        cameras.first,
        ResolutionPreset.high,
        imageFormatGroup: ImageFormatGroup.jpeg
    );        

The CameraController() is a widget from the camera package.

That is it! We are done. We just need to initialize our controller and emit and the CameraState{} so our UI can react to it and bring on the correct screen into view.

await cameraController.initialize();

    emit (CameraState(
        controller: cameraController,
        camera: cameraController.description
    ));        

In the emit statement you can see why we needed a controller and camera as variables for the CameraState{}.

The next methods are relatively easier.

To take a photo...

 takePicture() async {
//set the state as CameraState so we can access the controller variable. 
    final currentState = state as CameraState;
    final controller = currentState.controller;

    //take a picture with the given controller
    final rawFile = await controller.takePicture();
    final picture = File(rawFile.path);
    emit (PreviewState(file: picture));
  }        

To select the photo...

selectPhoto({
    required File file
  }){
    emit (SelectProfilePhotoState(file: file));
  }        

We need a method to switch the camera settings. For instance, switching to the front camera. Every time this is done we need to re-initialize our controller with the new settings.

 switchCameraOptions({
    required CameraController cameraController,
    bool? isBackCam,
    ResolutionPreset? resolutionPreset
  }) async {

    final camera = await availableCameras();
    cameraController = CameraController(
        isBackCam == true ? camera.last : camera.first,
        resolutionPreset ?? ResolutionPreset.high
    );

    await cameraController.initialize();
    emit(CameraState(
        controller: cameraController,
        camera: cameraController.description
    ));

  }        

We are only working with the camera resolution and with switching the camera from back to front or vice versa depending on the boolean, but you could add more of course.

We are saying here that;

  • If the boolean is true switch to the front camera else switch to the back camera.
  • If there is a specified resolution, use it, else, use the default. i.e ResolutionPreset.high
  • Initialize the controller and emit the CameraState{}.

At the moment we are not using any of these methods. To do so we need to build our UI --screens.

Build the App screens

We are getting there. Let's build our screens. This is the part of our app that the user will see and interact with.

the select photo screen...

class PhotoScreen extends StatelessWidget {
  const PhotoScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<PhotoAppCubit, PhotoAppState>(
        builder: (context, state){
          state as SelectProfilePhotoState;
          return Scaffold(
            appBar: AppBar(
              title: const Text("Take a Photo"),
              centerTitle: true,
            ),
            body: Column(
              children: [
                const SizedBox(height: 10,),
                Center(
                  child: Stack(
                    children: [
                      getAvatar(state.file),
                      Positioned(
                        bottom: -10,
                        left:  80,
                        child: IconButton(
                          onPressed: (){
                            context.read<PhotoAppCubit>().openCamera();
                          },
                          icon: const Icon(Icons.photo_camera_rounded)),
                    )
                  ],
              ),
                ),
            ],),
          );
        },
    );
  }
}

CircleAvatar getAvatar(File? displayImage){
  if(displayImage == null){
    return const CircleAvatar(
      radius: 65,
      backgroundImage: AssetImage('assets/avatar.png'),
    );
  } else {
    return CircleAvatar(
      radius: 65,
      backgroundImage: FileImage(displayImage),
    );
  }
}        

camera screen...

class CameraScreen extends StatefulWidget {
  const CameraScreen({super.key});

  @override
  State<CameraScreen> createState() => _CameraScreenState();
}

class _CameraScreenState extends State<CameraScreen> {
bool _isBackCamera = false;

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<PhotoAppCubit, PhotoAppState>(
        builder: (context, state){
          state as CameraState;
          return CameraPreview(
            state.controller,
            child: Scaffold(
              backgroundColor: Colors.transparent,
                body: Stack(
                  fit: StackFit.expand,
                  alignment: Alignment.bottomCenter,
                  children: [
                    Row(
                      crossAxisAlignment: CrossAxisAlignment.end,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        GestureDetector(
                          onTap: (){
                            context.read<PhotoAppCubit>().takePicture();
                          },
                          child: Container(
                            height: 100,
                            width: 70,
                            decoration: const BoxDecoration(
                                shape: BoxShape.circle,
                                color: Colors.white
                            ),
                          ),
                        ),
                        const SizedBox(width: 10,),
                        IconButton(
                            color: Colors.white,
                            padding: const EdgeInsets.only(bottom: 25),
                            onPressed: (){
                              setState(() {
                                _isBackCamera = !_isBackCamera;
                              });
                              context.read<PhotoAppCubit>().switchCameraOptions(
                                  isBackCam: _isBackCamera,
                                  cameraController: state.controller,
                               );
                            },
                            icon: const Icon(Icons.cameraswitch)
                        )
                      ],
                    ),
                  ],
                )
            ),
          );
        });
  }
}        

Observe how we have used the CameraPreview widget to show the preview from the camera. We have also set the background color of the Scaffold widget to transparent for this to work.

preview screen

class PreviewScreen extends StatelessWidget {
  const PreviewScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<PhotoAppCubit, PhotoAppState>(
      builder: (context,state) {
        state as PreviewState;
        return Material(
          child: DecoratedBox(
            decoration: BoxDecoration(
                image: DecorationImage(
                    fit: BoxFit.cover,
                    image: FileImage(state.file!)
                )
              ),
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.end,
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                InkWell(
                  onTap: (){
                    context.read<PhotoAppCubit>().openCamera();
                  },
                  child: Container(
                    height: 40,
                    width: 100,
                    color: Colors.white38,
                    child: const Icon(Icons.cancel_outlined),
                  ),
                ),
                const SizedBox(width: 20,),
                InkWell(
                  onTap: (){
                    context.read<PhotoAppCubit>().selectPhoto(file: state.file!);
                  },
                  child: Container(
                    height: 40,
                    width: 60,
                    color: Colors.white38,
                    child: const Icon(Icons.check_outlined),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    );
  }
}        

Write the routing logic

This will be the final piece of the jigsaw.

It will be responsible for shuffling our screens depending on the current state. This only works because we have created our cubit, states and screens beforehand. And now we are merging them together.

Create a file inside bloc and call it photo_app_logic.dart.

class PhotoAppLogic extends StatelessWidget {
  const PhotoAppLogic({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocConsumer<PhotoAppCubit, PhotoAppState>(
        builder: (context, state){
           if (state is SelectProfilePhotoState){
             return const PhotoScreen();
           }
           else if (state is CameraState){
             return const CameraScreen();
           }
           else if (state is PreviewState){
             return const PreviewScreen();
           }
           else {
             return const Scaffold(
               body: Center(
                 child: Text('Nothing to show'),
               ),
             );
           }
        },
        //Could be used with the Super state variables to handle loading dialogs and error messages but not implemented for simplicity sake.
        listener: (context, state ){
          //if (state.isLoading == false) popTheDialog;
          //if (state.isLoading == true) showLoadingDialog;
          //if (state.hasError == true) showErrorDialog;
        }
    );
  }
}
        

To make sure this works don't forget to provide the Cubit to your widget tree like this:

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter camera and Bloc',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: BlocProvider(
        create: (context) => PhotoAppCubit(),
        child: const PhotoAppLogic(),
      ),
    );
  }
}        

Test and voila!

select a photo screen




select a photo screen with image selected

You can find the full source code here


要查看或添加评论,请登录

Toby Iwarifgha的更多文章

社区洞察

其他会员也浏览了