diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 41077b35b449..3d8ca4fc6578 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.12.1 +* Fixes a crash where a `CameraController` could update its value after being disposed, throwing "A CameraController was used after being disposed." (see [flutter/flutter#184959](https://github.com/flutter/flutter/issues/184959)). * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 0.12.0+1 diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 05d52152b430..a930723761aa 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -337,7 +337,9 @@ class CameraController extends ValueNotifier { _deviceOrientationSubscription ??= CameraPlatform.instance .onDeviceOrientationChanged() .listen((DeviceOrientationChangedEvent event) { - value = value.copyWith(deviceOrientation: event.orientation); + if (!_isDisposed) { + value = value.copyWith(deviceOrientation: event.orientation); + } }); _cameraId = await CameraPlatform.instance.createCameraWithSettings( @@ -355,7 +357,9 @@ class CameraController extends ValueNotifier { unawaited( CameraPlatform.instance.onCameraError(_cameraId).first.then((CameraErrorEvent event) { - value = value.copyWith(errorDescription: event.description); + if (!_isDisposed) { + value = value.copyWith(errorDescription: event.description); + } }), ); @@ -364,25 +368,20 @@ class CameraController extends ValueNotifier { imageFormatGroup: imageFormatGroup ?? ImageFormatGroup.unknown, ); - value = value.copyWith( - isInitialized: true, - description: description, - previewSize: await initializeCompleter.future.then( - (CameraInitializedEvent event) => Size(event.previewWidth, event.previewHeight), - ), - exposureMode: await initializeCompleter.future.then( - (CameraInitializedEvent event) => event.exposureMode, - ), - focusMode: await initializeCompleter.future.then( - (CameraInitializedEvent event) => event.focusMode, - ), - exposurePointSupported: await initializeCompleter.future.then( - (CameraInitializedEvent event) => event.exposurePointSupported, - ), - focusPointSupported: await initializeCompleter.future.then( - (CameraInitializedEvent event) => event.focusPointSupported, - ), - ); + final CameraInitializedEvent event = await initializeCompleter.future; + + // The controller may be disposed while awaiting initialization above. + if (!_isDisposed) { + value = value.copyWith( + isInitialized: true, + description: description, + previewSize: Size(event.previewWidth, event.previewHeight), + exposureMode: event.exposureMode, + focusMode: event.focusMode, + exposurePointSupported: event.exposurePointSupported, + focusPointSupported: event.focusPointSupported, + ); + } } on PlatformException catch (e) { throw CameraException(e.code, e.message); } finally { diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index f2cecaf6a7bc..60157cd79be5 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing Dart. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.12.0+1 +version: 0.12.1 environment: sdk: ^3.10.0 diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 8366e41f6c2b..5ddb256513c9 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -3428,6 +3428,54 @@ void main() { expect(cameraController.value.errorDescription, mockOnCameraErrorEvent.description); }); }); + + group('does not update value after dispose', () { + test('when initialization completes after dispose', () async { + final platform = MockDelayedInitializeCameraPlatform(); + CameraPlatform.instance = platform; + + final cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + + final Future initializeFuture = cameraController.initialize(); + await pumpEventQueue(); + + // Dispose while initialization is still in flight, then let it complete. + final Future disposeFuture = cameraController.dispose(); + platform.initializeCameraCompleter.complete(); + + await expectLater(initializeFuture, completes); + await disposeFuture; + }); + + test('when a camera error event arrives after dispose', () async { + final platform = MockControllableErrorCameraPlatform(); + CameraPlatform.instance = platform; + + final cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + await cameraController.initialize(); + await cameraController.dispose(); + + // A camera error arriving after dispose must not update `value`. + platform.cameraErrorController.add(const CameraErrorEvent(13, 'late error')); + await pumpEventQueue(); + + expect(cameraController.value.errorDescription, isNull); + }); + }); } class MockCameraPlatform extends Mock with MockPlatformInterfaceMixin implements CameraPlatform { @@ -3611,3 +3659,26 @@ class MockCameraDescription extends CameraDescription { @override String get name => 'back'; } + +/// A platform whose [initializeCamera] only completes once +/// [initializeCameraCompleter] is completed, so a test can dispose the +/// controller while initialization is still in flight. +class MockDelayedInitializeCameraPlatform extends MockCameraPlatform { + final Completer initializeCameraCompleter = Completer(); + + @override + Future initializeCamera( + int? cameraId, { + ImageFormatGroup? imageFormatGroup = ImageFormatGroup.unknown, + }) => initializeCameraCompleter.future; +} + +/// A platform whose camera error events are driven by [cameraErrorController], +/// so a test can emit an error after the controller has been disposed. +class MockControllableErrorCameraPlatform extends MockCameraPlatform { + final StreamController cameraErrorController = + StreamController.broadcast(); + + @override + Stream onCameraError(int cameraId) => cameraErrorController.stream; +}