Navaron Bracke

refactor the MobileScanner widget to remove the lifecycle handling in the widget

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart';
import 'package:mobile_scanner/src/scan_window_calculation.dart';
/// The function signature for the error builder.
... ... @@ -17,18 +14,26 @@ typedef MobileScannerErrorBuilder = Widget Function(
Widget?,
);
/// The [MobileScanner] widget displays a live camera preview.
/// This widget displays a live camera preview for the barcode scanner.
class MobileScanner extends StatefulWidget {
/// The controller that manages the barcode scanner.
///
/// If this is null, the scanner will manage its own controller.
final MobileScannerController? controller;
/// Create a new [MobileScanner] using the provided [controller].
const MobileScanner({
required this.controller,
this.fit = BoxFit.cover,
this.errorBuilder,
this.overlayBuilder,
this.placeholderBuilder,
this.scanWindow,
super.key,
});
/// The controller for the camera preview.
final MobileScannerController controller;
/// The function that builds an error widget when the scanner
/// could not be started.
/// The error builder for the camera preview.
///
/// If this is null, defaults to a black [ColoredBox]
/// with a centered white [Icons.error] icon.
/// If this is null, a black [ColoredBox],
/// with a centered white [Icons.error] icon is used as error widget.
final MobileScannerErrorBuilder? errorBuilder;
/// The [BoxFit] for the camera preview.
... ... @@ -36,250 +41,123 @@ class MobileScanner extends StatefulWidget {
/// Defaults to [BoxFit.cover].
final BoxFit fit;
/// The function that signals when new codes were detected by the [controller].
final void Function(BarcodeCapture barcodes) onDetect;
/// The function that signals when the barcode scanner is started.
@Deprecated('Use onScannerStarted() instead.')
final void Function(MobileScannerArguments? arguments)? onStart;
/// The function that signals when the barcode scanner is started.
final void Function(MobileScannerArguments? arguments)? onScannerStarted;
/// The builder for the overlay above the camera preview.
///
/// The resulting widget can be combined with the [scanWindow] rectangle
/// to create a cutout for the camera preview.
///
/// The [BoxConstraints] for this builder
/// are the same constraints that are used to compute the effective [scanWindow].
///
/// The overlay is only displayed when the camera preview is visible.
final LayoutWidgetBuilder? overlayBuilder;
/// The function that builds a placeholder widget when the scanner
/// is not yet displaying its camera preview.
/// The placeholder builder for the camera preview.
///
/// If this is null, a black [ColoredBox] is used as placeholder.
///
/// The placeholder is displayed when the camera preview is being initialized.
final Widget Function(BuildContext, Widget?)? placeholderBuilder;
/// if set barcodes will only be scanned if they fall within this [Rect]
/// useful for having a cut-out overlay for example. these [Rect]
/// coordinates are relative to the widget size, so by how much your
/// rectangle overlays the actual image can depend on things like the
/// [BoxFit]
final Rect? scanWindow;
/// Only set this to true if you are starting another instance of mobile_scanner
/// right after disposing the first one, like in a PageView.
/// The scan window rectangle for the barcode scanner.
///
/// Default: false
final bool startDelay;
/// The overlay which will be painted above the scanner when has started successful.
/// Will no be pointed when an error occurs or the scanner hasn't been started yet.
final Widget? overlay;
/// Create a new [MobileScanner] using the provided [controller]
/// and [onBarcodeDetected] callback.
const MobileScanner({
this.controller,
this.errorBuilder,
this.fit = BoxFit.cover,
required this.onDetect,
@Deprecated('Use onScannerStarted() instead.') this.onStart,
this.onScannerStarted,
this.placeholderBuilder,
this.scanWindow,
this.startDelay = false,
this.overlay,
super.key,
});
/// If this is not null, the barcode scanner will only scan barcodes
/// which intersect this rectangle.
///
/// The rectangle is relative to the layout size of the *camera preview widget*,
/// rather than the actual camera preview size,
/// since the actual widget size might not be the same as the camera preview size.
///
/// For example, the applied [fit] has an effect on the size of the camera preview widget,
/// while the camera preview size remains the same.
final Rect? scanWindow;
@override
State<MobileScanner> createState() => _MobileScannerState();
}
class _MobileScannerState extends State<MobileScanner>
with WidgetsBindingObserver {
/// The subscription that listens to barcode detection.
StreamSubscription<BarcodeCapture>? _barcodesSubscription;
/// The internally managed controller.
late MobileScannerController _controller;
/// Whether the controller should resume
/// when the application comes back to the foreground.
bool _resumeFromBackground = false;
MobileScannerException? _startException;
Widget _buildPlaceholderOrError(BuildContext context, Widget? child) {
final error = _startException;
class _MobileScannerState extends State<MobileScanner> {
/// The current scan window.
Rect? scanWindow;
if (error != null) {
return widget.errorBuilder?.call(context, error, child) ??
const ColoredBox(
color: Colors.black,
child: Center(child: Icon(Icons.error, color: Colors.white)),
/// Recalculate the scan window based on the updated [constraints].
void _maybeUpdateScanWindow(MobileScannerState scannerState, BoxConstraints constraints) {
if (widget.scanWindow != null && scanWindow == null) {
scanWindow = calculateScanWindowRelativeToTextureInPercentage(
widget.fit,
widget.scanWindow!,
textureSize: scannerState.size,
widgetSize: constraints.biggest,
);
}
return widget.placeholderBuilder?.call(context, child) ??
const ColoredBox(color: Colors.black);
unawaited(widget.controller.updateScanWindow(scanWindow));
}
/// Start the given [scanner].
Future<void> _startScanner() async {
if (widget.startDelay) {
await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
}
_barcodesSubscription ??= _controller.barcodes.listen(
widget.onDetect,
);
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<MobileScannerState>(
valueListenable: widget.controller,
builder: (BuildContext context, MobileScannerState value, Widget? child) {
if (!value.isInitialized) {
const Widget defaultPlaceholder = ColoredBox(color: Colors.black);
if (!_controller.autoStart) {
debugPrint(
'mobile_scanner: not starting automatically because autoStart is set to false in the controller.',
);
return;
return widget.placeholderBuilder?.call(context, child) ?? defaultPlaceholder;
}
_controller.start().then((arguments) {
// ignore: deprecated_member_use_from_same_package
widget.onStart?.call(arguments);
widget.onScannerStarted?.call(arguments);
}).catchError((error) {
if (!mounted) {
return;
}
final MobileScannerException? error = value.error;
if (error is MobileScannerException) {
_startException = error;
} else if (error is PlatformException) {
_startException = MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
code: error.code,
message: error.message,
details: error.details,
),
);
} else {
_startException = MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
details: error,
),
if (error != null) {
const Widget defaultError = ColoredBox(
color: Colors.black,
child: Center(child: Icon(Icons.error, color: Colors.white)),
);
}
setState(() {});
});
return widget.errorBuilder?.call(context, error, child) ?? defaultError;
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_controller = widget.controller ?? MobileScannerController();
_startScanner();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// App state changed before the controller was initialized.
if (_controller.isStarting) {
return;
}
switch (state) {
case AppLifecycleState.resumed:
if (_resumeFromBackground) {
_startScanner();
}
case AppLifecycleState.inactive:
_resumeFromBackground = true;
_controller.stop();
default:
break;
}
}
Rect? scanWindow;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return ValueListenableBuilder<MobileScannerArguments?>(
valueListenable: _controller.startArguments,
builder: (context, value, child) {
if (value == null) {
return _buildPlaceholderOrError(context, child);
}
_maybeUpdateScanWindow(value, constraints);
if (widget.scanWindow != null && scanWindow == null) {
scanWindow = calculateScanWindowRelativeToTextureInPercentage(
widget.fit,
widget.scanWindow!,
textureSize: value.size,
widgetSize: constraints.biggest,
final Widget? overlay = widget.overlayBuilder?.call(context, constraints);
final Size cameraPreviewSize = value.size;
final Widget scannerWidget = ClipRect(
child: SizedBox.fromSize(
size: constraints.biggest,
child: FittedBox(
fit: widget.fit,
child: SizedBox(
width: cameraPreviewSize.width,
height: cameraPreviewSize.height,
child: MobileScannerPlatform.instance.buildCameraView(),
),
),
),
);
_controller.updateScanWindow(scanWindow);
if (overlay == null) {
return scannerWidget;
}
if (widget.overlay != null) {
return Stack(
alignment: Alignment.center,
children: [
_scanner(
value.size,
value.webId,
value.textureId,
value.numberOfCameras,
),
widget.overlay!,
children: <Widget>[
scannerWidget,
overlay,
],
);
} else {
return _scanner(
value.size,
value.webId,
value.textureId,
value.numberOfCameras,
);
}
},
);
},
);
}
Widget _scanner(
Size size,
String? webId,
int? textureId,
int? numberOfCameras,
) {
return ClipRect(
child: LayoutBuilder(
builder: (_, constraints) {
return SizedBox.fromSize(
size: constraints.biggest,
child: FittedBox(
fit: widget.fit,
child: SizedBox(
width: size.width,
height: size.height,
child: kIsWeb
? HtmlElementView(viewType: webId!)
: Texture(textureId: textureId!),
),
),
);
},
),
);
}
@override
void dispose() {
_controller.updateScanWindow(null);
WidgetsBinding.instance.removeObserver(this);
_barcodesSubscription?.cancel();
_barcodesSubscription = null;
_controller.dispose();
// When this widget is unmounted, reset the scan window.
widget.controller.updateScanWindow(null);
super.dispose();
}
}
... ...