Skip to content

Commit 1c88687

Browse files
Merge branch 'develop'
2 parents 3c5a9ac + 715c50f commit 1c88687

File tree

6 files changed

+172
-25
lines changed

6 files changed

+172
-25
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 7.0.1
2+
3+
* Added error handling for when `MobileScannerController.start` is called without an active `MobileScanner` widget.
4+
15
## 7.0.0
26

37
This version finalizes all changes from the beta and release candidate cycles and introduces major improvements, bug fixes, and breaking changes.
@@ -13,6 +17,7 @@ This version finalizes all changes from the beta and release candidate cycles an
1317
**Highlights**
1418

1519
* [iOS/macOS] Migrated to the Vision API with a unified Apple codebase.
20+
* [iOS] Minimum iOS version changed from 15 to 12.
1621
* [Android] Removed dependency on `kotlin-bom` and updated CameraX and camera-camera2 dependencies.
1722
* Support for pause/resume functionality across platforms.
1823
* `MobileScannerErrorCode` now includes readable error messages in debug mode.

android/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ buildscript {
99
}
1010

1111
dependencies {
12-
classpath 'com.android.tools.build:gradle:8.9.2'
12+
classpath 'com.android.tools.build:gradle:8.10.1'
1313
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1414
}
1515
}
@@ -78,5 +78,5 @@ dependencies {
7878
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
7979

8080
testImplementation 'org.jetbrains.kotlin:kotlin-test'
81-
testImplementation 'org.mockito:mockito-core:5.17.0'
81+
testImplementation 'org.mockito:mockito-core:5.18.0'
8282
}

lib/src/enums/mobile_scanner_error_code.dart

Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,122 @@ enum MobileScannerErrorCode {
4545
/// The controller is not attached to any widget.
4646
///
4747
/// This error occurs when [MobileScannerController.start] is called
48-
/// before the controller has been attached to a [MobileScanner] widget.
49-
///
50-
/// To fix this error:
51-
/// - Ensure that a [MobileScanner] widget is built and linked to the
52-
/// controller.
53-
/// - The [MobileScanner] widget automatically attaches the controller
54-
/// when it is initialized.
55-
/// - Wait until the widget is fully built before calling start.
48+
/// but there is no active [MobileScanner] widget
49+
/// for the [MobileScannerController] in the widget tree.
50+
///
51+
/// To avoid this error, ensure that a [MobileScanner] widget
52+
/// is attached to the widget tree
53+
/// when [MobileScannerController.start] is called.
54+
///
55+
/// BAD:
56+
///
57+
/// ```dart
58+
/// class ScannerExample extends StatefulWidget {
59+
/// const ScannerExample({super.key});
60+
///
61+
/// @override
62+
/// State<ScannerExample> createState() => _ScannerExampleState();
63+
/// }
64+
///
65+
/// class _ScannerExampleState extends State<ScannerExample> {
66+
/// final MobileScannerController controller = MobileScannerController();
67+
///
68+
/// bool _showScanner = false;
69+
///
70+
/// @override
71+
/// void initState() {
72+
/// super.initState();
73+
/// // The MobileScanner is only in the widget tree after the button is pressed.
74+
/// controller.start();
75+
/// }
76+
///
77+
/// @override
78+
/// Widget build(BuildContext context) {
79+
/// return Column(
80+
/// children: [
81+
/// ElevatedButton(
82+
/// onPressed: () {
83+
/// setState(() {
84+
/// _showScanner = true;
85+
/// });
86+
/// },
87+
/// child: const Text('Button'),
88+
/// ),
89+
/// if (_showScanner)
90+
/// Expanded(child: MobileScanner(controller: controller)),
91+
/// ],
92+
/// );
93+
/// }
94+
/// }
95+
/// ```
96+
///
97+
/// GOOD:
98+
///
99+
/// ```dart
100+
/// class ScannerExample extends StatefulWidget {
101+
/// const ScannerExample({super.key});
102+
///
103+
/// @override
104+
/// State<ScannerExample> createState() => _ScannerExampleState();
105+
/// }
106+
///
107+
/// class _ScannerExampleState extends State<ScannerExample> {
108+
/// final MobileScannerController controller = MobileScannerController();
109+
///
110+
/// @override
111+
/// Widget build(BuildContext context) {
112+
/// return Column(
113+
/// children: [
114+
/// ElevatedButton(
115+
/// // The MobileScanner is already in the widget tree.
116+
/// onPressed: controller.start,
117+
/// child: const Text('Button'),
118+
/// ),
119+
/// Expanded(child: MobileScanner(controller: controller)),
120+
/// ],
121+
/// );
122+
/// }
123+
/// }
124+
/// ```
125+
///
126+
/// GOOD:
127+
///
128+
/// ```dart
129+
/// class ScannerExample extends StatefulWidget {
130+
/// const ScannerExample({super.key});
131+
///
132+
/// @override
133+
/// State<ScannerExample> createState() => _ScannerExampleState();
134+
/// }
135+
///
136+
/// class _ScannerExampleState extends State<ScannerExample> {
137+
/// final MobileScannerController controller = MobileScannerController();
138+
///
139+
/// bool _showScanner = false;
140+
///
141+
/// void start() {
142+
/// if (_showScanner) return;
143+
///
144+
/// setState(() {
145+
/// _showScanner = true;
146+
/// });
147+
///
148+
/// // The MobileScanner will be in the widget tree in the next frame.
149+
/// controller.start();
150+
/// }
151+
///
152+
/// @override
153+
/// Widget build(BuildContext context) {
154+
/// return Column(
155+
/// children: [
156+
/// ElevatedButton(onPressed: start, child: const Text('Button')),
157+
/// if (_showScanner)
158+
/// Expanded(child: MobileScanner(controller: controller)),
159+
/// ],
160+
/// );
161+
/// }
162+
/// }
163+
/// ```
56164
controllerNotAttached;
57165

58166
/// Convert the given [PlatformException.code] to a [MobileScannerErrorCode].

lib/src/mobile_scanner.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ class _MobileScannerState extends State<MobileScanner>
328328
}
329329

330330
@override
331-
Future<void> dispose() async {
331+
void dispose() {
332332
super.dispose();
333333
unawaited(disposeMobileScanner());
334334
}

lib/src/mobile_scanner_controller.dart

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66
import 'package:flutter/foundation.dart';
77
import 'package:flutter/services.dart';
88
import 'package:flutter/widgets.dart';
9+
import 'package:meta/meta.dart';
910
import 'package:mobile_scanner/src/enums/barcode_format.dart';
1011
import 'package:mobile_scanner/src/enums/camera_facing.dart';
1112
import '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
}

pubspec.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: mobile_scanner
22
description: A universal Flutter barcode and QR code scanner using CameraX/ML Kit for Android, AVFoundation/Apple Vision for iOS & macOS, and ZXing for web.
3-
version: 7.0.0
3+
version: 7.0.1
44
repository: https://github.com/juliansteenbakker/mobile_scanner
55

66
screenshots:
@@ -21,14 +21,15 @@ dependencies:
2121
sdk: flutter
2222
flutter_web_plugins:
2323
sdk: flutter
24+
meta: ">=1.16.0 <2.0.0"
2425
plugin_platform_interface: ^2.0.2
2526
web: ">=0.5.1 <2.0.0"
2627

2728
dev_dependencies:
2829
flutter_test:
2930
sdk: flutter
3031
mocktail: ^1.0.4
31-
very_good_analysis: ^7.0.0
32+
very_good_analysis: ">=7.0.0 <9.0.0"
3233

3334
flutter:
3435
plugin:

0 commit comments

Comments
 (0)