Using a Stack containing a QR code scanner & a CameraPreview leads to preview freezing

Viewed 113

I initially tried to find a library that would allow for a single camera component that can do both, qr code scanning & allowing the user to take a normal picture with it. I also came across this post that went unanswered (if you have any suggestions, feel free to comment them below), so I decided to use two different components in a stack and play with their visibility.

I'm using the mobile_scanner library for the QR code scanner and the official docs for the picture taking functionality.

The idea (until a widget that can do both shows up) is that initially, the user will see the CameraPreview() widget, and when toggling the switch to the ON state, they will see the MobileScanner() one. The issue that occurs, is that although the MobileScanner widget seems to handle its visibility changes normally (meaning, pausing & resuming the camera), the CameraPreview one cannot. If I toggle the switch on and then toggle it off again, the camera preview is frozen. I'm having a hard time understanding whether I should be manually calling _controller.resumePreview() and at what point exactly.

Here's the screen code:

class CameraComponent extends StatefulWidget {
  const CameraComponent({Key? key}) : super(key: key);

  @override
  State<CameraComponent> createState() => _CameraComponentState();
}

class _CameraComponentState extends State<CameraComponent> {
  CameraCubit get _cubit => context.read<CameraCubit>();

  bool qrModeEnabled = false;
  CameraController? _controller;
  late Future<void> _initializeControllerFuture;
  Photo? _artwork;
  final ArtworkRepository _artworkRepository = getIt<ArtworkRepository>();

  @override
  void initState() {
    super.initState();
    _cubit.init();
  }

  @override
  void dispose() {
    // Dispose of the controller when the widget is disposed.
    _controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: BlocConsumer<CameraCubit, CameraState>(
        listener: (BuildContext context, CameraState state) {
          state.maybeWhen(
            initCamera: (List<CameraDescription> cameras) {
              _controller = CameraController(
                cameras.firstWhere(
                  (CameraDescription camera) =>
                      camera.lensDirection == CameraLensDirection.back,
                  orElse: () => cameras.first,
                ),
                ResolutionPreset.max,
              );
              _initializeControllerFuture = _controller!.initialize();
            },
            orElse: () {},
          );
        },
        builder: (BuildContext context, CameraState state) {
          return state.maybeWhen(
            initCamera: (_) {
              return FutureBuilder<void>(
                future: _initializeControllerFuture,
                builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
                  if (snapshot.connectionState == ConnectionState.done) {
                    return Stack(
                      children: <Widget>[
                        Visibility(
                          visible: qrModeEnabled,
                          child: MobileScanner(
                            onDetect: (Barcode barcode,
                                MobileScannerArguments? args) async {
                              if (barcode.rawValue == null) {
                                debugPrint('Failed to scan qr code');
                              } else {
                                try {
                                  final int artworkId = int.parse(
                                      barcode.rawValue!.split('_')[1]);

                                  debugPrint(
                                      'QR code found! ${barcode.rawValue!}');

                                  _artwork = _artworkRepository.savedArtworks
                                      .firstWhereOrNull(
                                    (Photo element) => element.id == artworkId,
                                  );

                                  _artwork ??= await _artworkRepository
                                      .getArtworkDetails(id: artworkId);

                                  NavigatorUtils.goToArtworkViewScreen(
                                    context,
                                    artwork: _artwork!,
                                  );
                                } catch (e) {
                                  rethrow;
                                }
                              }
                            },
                          ),
                        ),
                        Visibility(
                          visible: !qrModeEnabled,
                          child: CameraPreview(_controller!),
                        ),
                        Align(
                          child: Image.asset('assets/icons/b_logo_grey.png'),
                        ),
                        Align(
                          alignment: Alignment.bottomCenter,
                          child: Padding(
                            padding: const EdgeInsets.only(bottom: 144),
                            child: Text(
                              'Scan a QR code or take a photo of the artwork',
                              style: montserratRegular15.copyWith(
                                color: AppColors.white,
                              ),
                            ),
                          ),
                        ),
                        Align(
                          alignment: Alignment.bottomCenter,
                          child: Padding(
                            padding: const EdgeInsets.only(bottom: 80),
                            child: GestureDetector(
                              onTap: () async {
                                final XFile image =
                                    await _controller!.takePicture();
                                final Uint8List bytes =
                                    await image.readAsBytes();
                                final String base64Image = base64Encode(bytes);

                                _cubit.addBase64Image(base64Image: base64Image);
                              },
                              child: Image.asset(
                                'assets/icons/shutter_icon.png',
                                height: 48,
                              ),
                            ),
                          ),
                        ),
                        Align(
                          alignment: Alignment.topRight,
                          child: Padding(
                            padding: const EdgeInsets.only(top: 10, right: 10),
                            child: Switch(
                              value: qrModeEnabled,
                              onChanged: (bool value) {
                                setState(() {
                                  qrModeEnabled = !qrModeEnabled;
                                });
                              },
                              activeTrackColor: AppColors.red,
                              inactiveTrackColor: AppColors.white,
                            ),
                          ),
                        ),
                      ],
                    );
                  } else {
                    return const Center(child: CircularProgressIndicator());
                  }
                },
              );
            },
            orElse: () => const SizedBox.shrink(),
          );
        },
      ),
    );
  }
}

and the CameraCubit.dart file:

class CameraCubit extends Cubit<CameraState> {
  CameraCubit({
    required ClarifaiRepository clarifaiRepository,
  })  : _clarifaiRepository = clarifaiRepository,
        super(const CameraState.initial());

  final ClarifaiRepository _clarifaiRepository;

  Future<void> init() async {
    try {
      final List<CameraDescription> cameras = await availableCameras();
      if (cameras.isNotEmpty) {
        emit(CameraState.initCamera(cameras: cameras));
      }
    } catch (e) {
      emit(CameraState.error(e));
    }
  }

  Future<void> addBase64Image({required String base64Image}) async {
    emit(const CameraState.loading());

    try {
      await _clarifaiRepository.addBase64Image(base64Image: base64Image);

      emit(const CameraState.success());
    } catch (e) {
      emit(CameraState.error(e));
    }
  }
}
1 Answers

I haven't used the Mobile Scanner library before. Instead, I have used the qr_code_scanner dependency for QR codes and it has a reassemble method like this:

 @override
 void reassemble() {
   super.reassemble();
   if (Platform.isAndroid) {
     controller!.pauseCamera();
   } else if (Platform.isIOS) {
     controller!.resumeCamera();
   }
 }

If this doesn't help, you can also manually pause/resume when the button is pressed with the method you mentioned in the end.

Related