@@ -6,6 +6,7 @@ import 'dart:async';
66import 'package:flutter/foundation.dart' ;
77import 'package:flutter/services.dart' ;
88import 'package:flutter/widgets.dart' ;
9+ import 'package:meta/meta.dart' ;
910import 'package:mobile_scanner/src/enums/barcode_format.dart' ;
1011import 'package:mobile_scanner/src/enums/camera_facing.dart' ;
1112import 'package:mobile_scanner/src/enums/detection_speed.dart' ;
@@ -128,7 +129,10 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
128129 StreamSubscription <DeviceOrientation >? _deviceOrientationSubscription;
129130
130131 bool _isDisposed = false ;
131- bool _isAttached = false ;
132+ // This completer keeps track of whether the MobileScanner widget,
133+ // that is attached to this controller,
134+ // called its `initState()` lifecycle method.
135+ final Completer <void > _isAttachedCompleter = Completer <void >();
132136
133137 void _disposeListeners () {
134138 _barcodesSubscription? .cancel ();
@@ -336,13 +340,35 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
336340 );
337341 }
338342
339- if (! _isAttached) {
340- throw MobileScannerException (
341- errorCode: MobileScannerErrorCode .controllerNotAttached,
342- errorDetails: MobileScannerErrorDetails (
343- message: MobileScannerErrorCode .controllerNotAttached.message,
344- ),
345- );
343+ // If start was called before the MobileScanner widget
344+ // had a chance to call its initState method,
345+ // wait for it to be called, using a timeout.
346+ if (! _isAttachedCompleter.isCompleted) {
347+ // The timeout is currently an arbitrary value,
348+ // which should be long enough for the next frame
349+ // to propagate any pending changes to the widget tree.
350+ await _isAttachedCompleter.future
351+ .timeout (const Duration (milliseconds: 500 ))
352+ .catchError ((Object error, StackTrace stackTrace) {
353+ throw MobileScannerException (
354+ errorCode: MobileScannerErrorCode .controllerNotAttached,
355+ errorDetails: MobileScannerErrorDetails (
356+ message: MobileScannerErrorCode .controllerNotAttached.message,
357+ details: stackTrace.toString (),
358+ ),
359+ );
360+ });
361+
362+ // Abort if the controller was disposed
363+ // while waiting for the widget to be attached.
364+ if (_isDisposed) {
365+ throw MobileScannerException (
366+ errorCode: MobileScannerErrorCode .controllerDisposed,
367+ errorDetails: MobileScannerErrorDetails (
368+ message: MobileScannerErrorCode .controllerDisposed.message,
369+ ),
370+ );
371+ }
346372 }
347373
348374 if (cameraDirection == CameraFacing .unknown) {
@@ -531,16 +557,23 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
531557 }
532558
533559 _isDisposed = true ;
534- _isAttached = false ;
535560 unawaited (_barcodesController.close ());
536561 super .dispose ();
537562
538563 await MobileScannerPlatform .instance.dispose ();
539564 }
540565
541- /// Keeps track if the controller is correctly attached to the MobileScanner
542- /// widget.
566+ /// Signal to this [MobileScannerController] that it is attached
567+ /// to a [MobileScanner] widget.
568+ ///
569+ /// This method is called by `_MobileScannerState.initState()`
570+ /// and is not intended to be used directly.
571+ @internal
543572 void attach () {
544- _isAttached = true ;
573+ if (_isAttachedCompleter.isCompleted) {
574+ return ;
575+ }
576+
577+ _isAttachedCompleter.complete ();
545578 }
546579}
0 commit comments