refactor the MobileScanner widget to remove the lifecycle handling in the widget
Showing
1 changed file
with
93 additions
and
215 deletions
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | 2 | ||
| 3 | -import 'package:flutter/foundation.dart'; | ||
| 4 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 5 | -import 'package:flutter/services.dart'; | ||
| 6 | -import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; | ||
| 7 | import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; | 4 | import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; |
| 8 | import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; | 5 | import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; |
| 9 | -import 'package:mobile_scanner/src/objects/barcode_capture.dart'; | ||
| 10 | -import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart'; | 6 | +import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; |
| 7 | +import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart'; | ||
| 11 | import 'package:mobile_scanner/src/scan_window_calculation.dart'; | 8 | import 'package:mobile_scanner/src/scan_window_calculation.dart'; |
| 12 | 9 | ||
| 13 | /// The function signature for the error builder. | 10 | /// The function signature for the error builder. |
| @@ -17,18 +14,26 @@ typedef MobileScannerErrorBuilder = Widget Function( | @@ -17,18 +14,26 @@ typedef MobileScannerErrorBuilder = Widget Function( | ||
| 17 | Widget?, | 14 | Widget?, |
| 18 | ); | 15 | ); |
| 19 | 16 | ||
| 20 | -/// The [MobileScanner] widget displays a live camera preview. | 17 | +/// This widget displays a live camera preview for the barcode scanner. |
| 21 | class MobileScanner extends StatefulWidget { | 18 | class MobileScanner extends StatefulWidget { |
| 22 | - /// The controller that manages the barcode scanner. | ||
| 23 | - /// | ||
| 24 | - /// If this is null, the scanner will manage its own controller. | ||
| 25 | - final MobileScannerController? controller; | 19 | + /// Create a new [MobileScanner] using the provided [controller]. |
| 20 | + const MobileScanner({ | ||
| 21 | + required this.controller, | ||
| 22 | + this.fit = BoxFit.cover, | ||
| 23 | + this.errorBuilder, | ||
| 24 | + this.overlayBuilder, | ||
| 25 | + this.placeholderBuilder, | ||
| 26 | + this.scanWindow, | ||
| 27 | + super.key, | ||
| 28 | + }); | ||
| 29 | + | ||
| 30 | + /// The controller for the camera preview. | ||
| 31 | + final MobileScannerController controller; | ||
| 26 | 32 | ||
| 27 | - /// The function that builds an error widget when the scanner | ||
| 28 | - /// could not be started. | 33 | + /// The error builder for the camera preview. |
| 29 | /// | 34 | /// |
| 30 | - /// If this is null, defaults to a black [ColoredBox] | ||
| 31 | - /// with a centered white [Icons.error] icon. | 35 | + /// If this is null, a black [ColoredBox], |
| 36 | + /// with a centered white [Icons.error] icon is used as error widget. | ||
| 32 | final MobileScannerErrorBuilder? errorBuilder; | 37 | final MobileScannerErrorBuilder? errorBuilder; |
| 33 | 38 | ||
| 34 | /// The [BoxFit] for the camera preview. | 39 | /// The [BoxFit] for the camera preview. |
| @@ -36,250 +41,123 @@ class MobileScanner extends StatefulWidget { | @@ -36,250 +41,123 @@ class MobileScanner extends StatefulWidget { | ||
| 36 | /// Defaults to [BoxFit.cover]. | 41 | /// Defaults to [BoxFit.cover]. |
| 37 | final BoxFit fit; | 42 | final BoxFit fit; |
| 38 | 43 | ||
| 39 | - /// The function that signals when new codes were detected by the [controller]. | ||
| 40 | - final void Function(BarcodeCapture barcodes) onDetect; | ||
| 41 | - | ||
| 42 | - /// The function that signals when the barcode scanner is started. | ||
| 43 | - @Deprecated('Use onScannerStarted() instead.') | ||
| 44 | - final void Function(MobileScannerArguments? arguments)? onStart; | ||
| 45 | - | ||
| 46 | - /// The function that signals when the barcode scanner is started. | ||
| 47 | - final void Function(MobileScannerArguments? arguments)? onScannerStarted; | 44 | + /// The builder for the overlay above the camera preview. |
| 45 | + /// | ||
| 46 | + /// The resulting widget can be combined with the [scanWindow] rectangle | ||
| 47 | + /// to create a cutout for the camera preview. | ||
| 48 | + /// | ||
| 49 | + /// The [BoxConstraints] for this builder | ||
| 50 | + /// are the same constraints that are used to compute the effective [scanWindow]. | ||
| 51 | + /// | ||
| 52 | + /// The overlay is only displayed when the camera preview is visible. | ||
| 53 | + final LayoutWidgetBuilder? overlayBuilder; | ||
| 48 | 54 | ||
| 49 | - /// The function that builds a placeholder widget when the scanner | ||
| 50 | - /// is not yet displaying its camera preview. | 55 | + /// The placeholder builder for the camera preview. |
| 51 | /// | 56 | /// |
| 52 | /// If this is null, a black [ColoredBox] is used as placeholder. | 57 | /// If this is null, a black [ColoredBox] is used as placeholder. |
| 58 | + /// | ||
| 59 | + /// The placeholder is displayed when the camera preview is being initialized. | ||
| 53 | final Widget Function(BuildContext, Widget?)? placeholderBuilder; | 60 | final Widget Function(BuildContext, Widget?)? placeholderBuilder; |
| 54 | 61 | ||
| 55 | - /// if set barcodes will only be scanned if they fall within this [Rect] | ||
| 56 | - /// useful for having a cut-out overlay for example. these [Rect] | ||
| 57 | - /// coordinates are relative to the widget size, so by how much your | ||
| 58 | - /// rectangle overlays the actual image can depend on things like the | ||
| 59 | - /// [BoxFit] | ||
| 60 | - final Rect? scanWindow; | ||
| 61 | - | ||
| 62 | - /// Only set this to true if you are starting another instance of mobile_scanner | ||
| 63 | - /// right after disposing the first one, like in a PageView. | 62 | + /// The scan window rectangle for the barcode scanner. |
| 64 | /// | 63 | /// |
| 65 | - /// Default: false | ||
| 66 | - final bool startDelay; | ||
| 67 | - | ||
| 68 | - /// The overlay which will be painted above the scanner when has started successful. | ||
| 69 | - /// Will no be pointed when an error occurs or the scanner hasn't been started yet. | ||
| 70 | - final Widget? overlay; | ||
| 71 | - | ||
| 72 | - /// Create a new [MobileScanner] using the provided [controller] | ||
| 73 | - /// and [onBarcodeDetected] callback. | ||
| 74 | - const MobileScanner({ | ||
| 75 | - this.controller, | ||
| 76 | - this.errorBuilder, | ||
| 77 | - this.fit = BoxFit.cover, | ||
| 78 | - required this.onDetect, | ||
| 79 | - @Deprecated('Use onScannerStarted() instead.') this.onStart, | ||
| 80 | - this.onScannerStarted, | ||
| 81 | - this.placeholderBuilder, | ||
| 82 | - this.scanWindow, | ||
| 83 | - this.startDelay = false, | ||
| 84 | - this.overlay, | ||
| 85 | - super.key, | ||
| 86 | - }); | 64 | + /// If this is not null, the barcode scanner will only scan barcodes |
| 65 | + /// which intersect this rectangle. | ||
| 66 | + /// | ||
| 67 | + /// The rectangle is relative to the layout size of the *camera preview widget*, | ||
| 68 | + /// rather than the actual camera preview size, | ||
| 69 | + /// since the actual widget size might not be the same as the camera preview size. | ||
| 70 | + /// | ||
| 71 | + /// For example, the applied [fit] has an effect on the size of the camera preview widget, | ||
| 72 | + /// while the camera preview size remains the same. | ||
| 73 | + final Rect? scanWindow; | ||
| 87 | 74 | ||
| 88 | @override | 75 | @override |
| 89 | State<MobileScanner> createState() => _MobileScannerState(); | 76 | State<MobileScanner> createState() => _MobileScannerState(); |
| 90 | } | 77 | } |
| 91 | 78 | ||
| 92 | -class _MobileScannerState extends State<MobileScanner> | ||
| 93 | - with WidgetsBindingObserver { | ||
| 94 | - /// The subscription that listens to barcode detection. | ||
| 95 | - StreamSubscription<BarcodeCapture>? _barcodesSubscription; | ||
| 96 | - | ||
| 97 | - /// The internally managed controller. | ||
| 98 | - late MobileScannerController _controller; | ||
| 99 | - | ||
| 100 | - /// Whether the controller should resume | ||
| 101 | - /// when the application comes back to the foreground. | ||
| 102 | - bool _resumeFromBackground = false; | ||
| 103 | - | ||
| 104 | - MobileScannerException? _startException; | ||
| 105 | - | ||
| 106 | - Widget _buildPlaceholderOrError(BuildContext context, Widget? child) { | ||
| 107 | - final error = _startException; | 79 | +class _MobileScannerState extends State<MobileScanner> { |
| 80 | + /// The current scan window. | ||
| 81 | + Rect? scanWindow; | ||
| 108 | 82 | ||
| 109 | - if (error != null) { | ||
| 110 | - return widget.errorBuilder?.call(context, error, child) ?? | ||
| 111 | - const ColoredBox( | ||
| 112 | - color: Colors.black, | ||
| 113 | - child: Center(child: Icon(Icons.error, color: Colors.white)), | 83 | + /// Recalculate the scan window based on the updated [constraints]. |
| 84 | + void _maybeUpdateScanWindow(MobileScannerState scannerState, BoxConstraints constraints) { | ||
| 85 | + if (widget.scanWindow != null && scanWindow == null) { | ||
| 86 | + scanWindow = calculateScanWindowRelativeToTextureInPercentage( | ||
| 87 | + widget.fit, | ||
| 88 | + widget.scanWindow!, | ||
| 89 | + textureSize: scannerState.size, | ||
| 90 | + widgetSize: constraints.biggest, | ||
| 114 | ); | 91 | ); |
| 115 | - } | ||
| 116 | 92 | ||
| 117 | - return widget.placeholderBuilder?.call(context, child) ?? | ||
| 118 | - const ColoredBox(color: Colors.black); | 93 | + unawaited(widget.controller.updateScanWindow(scanWindow)); |
| 119 | } | 94 | } |
| 120 | - | ||
| 121 | - /// Start the given [scanner]. | ||
| 122 | - Future<void> _startScanner() async { | ||
| 123 | - if (widget.startDelay) { | ||
| 124 | - await Future.delayed(const Duration(seconds: 1, milliseconds: 500)); | ||
| 125 | } | 95 | } |
| 126 | 96 | ||
| 127 | - _barcodesSubscription ??= _controller.barcodes.listen( | ||
| 128 | - widget.onDetect, | ||
| 129 | - ); | 97 | + @override |
| 98 | + Widget build(BuildContext context) { | ||
| 99 | + return ValueListenableBuilder<MobileScannerState>( | ||
| 100 | + valueListenable: widget.controller, | ||
| 101 | + builder: (BuildContext context, MobileScannerState value, Widget? child) { | ||
| 102 | + if (!value.isInitialized) { | ||
| 103 | + const Widget defaultPlaceholder = ColoredBox(color: Colors.black); | ||
| 130 | 104 | ||
| 131 | - if (!_controller.autoStart) { | ||
| 132 | - debugPrint( | ||
| 133 | - 'mobile_scanner: not starting automatically because autoStart is set to false in the controller.', | ||
| 134 | - ); | ||
| 135 | - return; | 105 | + return widget.placeholderBuilder?.call(context, child) ?? defaultPlaceholder; |
| 136 | } | 106 | } |
| 137 | 107 | ||
| 138 | - _controller.start().then((arguments) { | ||
| 139 | - // ignore: deprecated_member_use_from_same_package | ||
| 140 | - widget.onStart?.call(arguments); | ||
| 141 | - widget.onScannerStarted?.call(arguments); | ||
| 142 | - }).catchError((error) { | ||
| 143 | - if (!mounted) { | ||
| 144 | - return; | ||
| 145 | - } | 108 | + final MobileScannerException? error = value.error; |
| 146 | 109 | ||
| 147 | - if (error is MobileScannerException) { | ||
| 148 | - _startException = error; | ||
| 149 | - } else if (error is PlatformException) { | ||
| 150 | - _startException = MobileScannerException( | ||
| 151 | - errorCode: MobileScannerErrorCode.genericError, | ||
| 152 | - errorDetails: MobileScannerErrorDetails( | ||
| 153 | - code: error.code, | ||
| 154 | - message: error.message, | ||
| 155 | - details: error.details, | ||
| 156 | - ), | ||
| 157 | - ); | ||
| 158 | - } else { | ||
| 159 | - _startException = MobileScannerException( | ||
| 160 | - errorCode: MobileScannerErrorCode.genericError, | ||
| 161 | - errorDetails: MobileScannerErrorDetails( | ||
| 162 | - details: error, | ||
| 163 | - ), | 110 | + if (error != null) { |
| 111 | + const Widget defaultError = ColoredBox( | ||
| 112 | + color: Colors.black, | ||
| 113 | + child: Center(child: Icon(Icons.error, color: Colors.white)), | ||
| 164 | ); | 114 | ); |
| 165 | - } | ||
| 166 | 115 | ||
| 167 | - setState(() {}); | ||
| 168 | - }); | 116 | + return widget.errorBuilder?.call(context, error, child) ?? defaultError; |
| 169 | } | 117 | } |
| 170 | 118 | ||
| 171 | - @override | ||
| 172 | - void initState() { | ||
| 173 | - super.initState(); | ||
| 174 | - WidgetsBinding.instance.addObserver(this); | ||
| 175 | - _controller = widget.controller ?? MobileScannerController(); | ||
| 176 | - _startScanner(); | ||
| 177 | - } | ||
| 178 | - | ||
| 179 | - @override | ||
| 180 | - void didChangeAppLifecycleState(AppLifecycleState state) { | ||
| 181 | - // App state changed before the controller was initialized. | ||
| 182 | - if (_controller.isStarting) { | ||
| 183 | - return; | ||
| 184 | - } | ||
| 185 | - | ||
| 186 | - switch (state) { | ||
| 187 | - case AppLifecycleState.resumed: | ||
| 188 | - if (_resumeFromBackground) { | ||
| 189 | - _startScanner(); | ||
| 190 | - } | ||
| 191 | - case AppLifecycleState.inactive: | ||
| 192 | - _resumeFromBackground = true; | ||
| 193 | - _controller.stop(); | ||
| 194 | - default: | ||
| 195 | - break; | ||
| 196 | - } | ||
| 197 | - } | ||
| 198 | - | ||
| 199 | - Rect? scanWindow; | ||
| 200 | - | ||
| 201 | - @override | ||
| 202 | - Widget build(BuildContext context) { | ||
| 203 | return LayoutBuilder( | 119 | return LayoutBuilder( |
| 204 | builder: (context, constraints) { | 120 | builder: (context, constraints) { |
| 205 | - return ValueListenableBuilder<MobileScannerArguments?>( | ||
| 206 | - valueListenable: _controller.startArguments, | ||
| 207 | - builder: (context, value, child) { | ||
| 208 | - if (value == null) { | ||
| 209 | - return _buildPlaceholderOrError(context, child); | ||
| 210 | - } | 121 | + _maybeUpdateScanWindow(value, constraints); |
| 211 | 122 | ||
| 212 | - if (widget.scanWindow != null && scanWindow == null) { | ||
| 213 | - scanWindow = calculateScanWindowRelativeToTextureInPercentage( | ||
| 214 | - widget.fit, | ||
| 215 | - widget.scanWindow!, | ||
| 216 | - textureSize: value.size, | ||
| 217 | - widgetSize: constraints.biggest, | 123 | + final Widget? overlay = widget.overlayBuilder?.call(context, constraints); |
| 124 | + final Size cameraPreviewSize = value.size; | ||
| 125 | + | ||
| 126 | + final Widget scannerWidget = ClipRect( | ||
| 127 | + child: SizedBox.fromSize( | ||
| 128 | + size: constraints.biggest, | ||
| 129 | + child: FittedBox( | ||
| 130 | + fit: widget.fit, | ||
| 131 | + child: SizedBox( | ||
| 132 | + width: cameraPreviewSize.width, | ||
| 133 | + height: cameraPreviewSize.height, | ||
| 134 | + child: MobileScannerPlatform.instance.buildCameraView(), | ||
| 135 | + ), | ||
| 136 | + ), | ||
| 137 | + ), | ||
| 218 | ); | 138 | ); |
| 219 | 139 | ||
| 220 | - _controller.updateScanWindow(scanWindow); | 140 | + if (overlay == null) { |
| 141 | + return scannerWidget; | ||
| 221 | } | 142 | } |
| 222 | - if (widget.overlay != null) { | 143 | + |
| 223 | return Stack( | 144 | return Stack( |
| 224 | alignment: Alignment.center, | 145 | alignment: Alignment.center, |
| 225 | - children: [ | ||
| 226 | - _scanner( | ||
| 227 | - value.size, | ||
| 228 | - value.webId, | ||
| 229 | - value.textureId, | ||
| 230 | - value.numberOfCameras, | ||
| 231 | - ), | ||
| 232 | - widget.overlay!, | 146 | + children: <Widget>[ |
| 147 | + scannerWidget, | ||
| 148 | + overlay, | ||
| 233 | ], | 149 | ], |
| 234 | ); | 150 | ); |
| 235 | - } else { | ||
| 236 | - return _scanner( | ||
| 237 | - value.size, | ||
| 238 | - value.webId, | ||
| 239 | - value.textureId, | ||
| 240 | - value.numberOfCameras, | ||
| 241 | - ); | ||
| 242 | - } | ||
| 243 | }, | 151 | }, |
| 244 | ); | 152 | ); |
| 245 | }, | 153 | }, |
| 246 | ); | 154 | ); |
| 247 | } | 155 | } |
| 248 | 156 | ||
| 249 | - Widget _scanner( | ||
| 250 | - Size size, | ||
| 251 | - String? webId, | ||
| 252 | - int? textureId, | ||
| 253 | - int? numberOfCameras, | ||
| 254 | - ) { | ||
| 255 | - return ClipRect( | ||
| 256 | - child: LayoutBuilder( | ||
| 257 | - builder: (_, constraints) { | ||
| 258 | - return SizedBox.fromSize( | ||
| 259 | - size: constraints.biggest, | ||
| 260 | - child: FittedBox( | ||
| 261 | - fit: widget.fit, | ||
| 262 | - child: SizedBox( | ||
| 263 | - width: size.width, | ||
| 264 | - height: size.height, | ||
| 265 | - child: kIsWeb | ||
| 266 | - ? HtmlElementView(viewType: webId!) | ||
| 267 | - : Texture(textureId: textureId!), | ||
| 268 | - ), | ||
| 269 | - ), | ||
| 270 | - ); | ||
| 271 | - }, | ||
| 272 | - ), | ||
| 273 | - ); | ||
| 274 | - } | ||
| 275 | - | ||
| 276 | @override | 157 | @override |
| 277 | void dispose() { | 158 | void dispose() { |
| 278 | - _controller.updateScanWindow(null); | ||
| 279 | - WidgetsBinding.instance.removeObserver(this); | ||
| 280 | - _barcodesSubscription?.cancel(); | ||
| 281 | - _barcodesSubscription = null; | ||
| 282 | - _controller.dispose(); | 159 | + // When this widget is unmounted, reset the scan window. |
| 160 | + widget.controller.updateScanWindow(null); | ||
| 283 | super.dispose(); | 161 | super.dispose(); |
| 284 | } | 162 | } |
| 285 | } | 163 | } |
-
Please register or login to post a comment