Skip to content

Commit 713817a

Browse files
stuartmorganyutaaraki-toydium
authored andcommitted
[camera] Fix exception in registerWith (flutter#6009)
Fixes a regression from an unintented change in behavior during the conversion to an in-app method channel for Android and iOS. Although the Dart code for their implementations is almost identical to the shared method channel version, the differences in initialization paths caused the platform versions to try to use the widget bindings before they had been set up: The constructor for a `dartPluginClass` is called during `registerWith`, which is before `main`, but the constructor for the default implementation isn't called until `CameraPlatform.instance` is called, since Dart automatically does lazy static class initializtion. To avoid the issue without forcing bindings to be initialized early, this makes setting up the platform channel listener lazily. Fixes flutter/flutter#106236
1 parent 799aa89 commit 713817a

File tree

8 files changed

+98
-54
lines changed

8 files changed

+98
-54
lines changed

packages/camera/camera_android/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.8+2
2+
3+
* Fixes exception in registerWith caused by the switch to an in-package method channel.
4+
15
## 0.9.8+1
26

37
* Ignores deprecation warnings for upcoming styleFrom button API changes.

packages/camera/camera_android/lib/src/android_camera.dart

+18-20
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,19 @@ const MethodChannel _channel =
1919

2020
/// The Android implementation of [CameraPlatform] that uses method channels.
2121
class AndroidCamera extends CameraPlatform {
22-
/// Construct a new method channel camera instance.
23-
AndroidCamera() {
24-
const MethodChannel channel =
25-
MethodChannel('plugins.flutter.io/camera_android/fromPlatform');
26-
channel.setMethodCallHandler(
27-
(MethodCall call) => handleDeviceMethodCall(call));
28-
}
29-
3022
/// Registers this class as the default instance of [CameraPlatform].
3123
static void registerWith() {
3224
CameraPlatform.instance = AndroidCamera();
3325
}
3426

3527
final Map<int, MethodChannel> _channels = <int, MethodChannel>{};
3628

29+
/// The name of the channel that device events from the platform side are
30+
/// sent on.
31+
@visibleForTesting
32+
static const String deviceEventChannelName =
33+
'plugins.flutter.io/camera_android/fromPlatform';
34+
3735
/// The controller we need to broadcast the different events coming
3836
/// from handleMethodCall, specific to camera events.
3937
///
@@ -50,11 +48,15 @@ class AndroidCamera extends CameraPlatform {
5048
///
5149
/// It is a `broadcast` because multiple controllers will connect to
5250
/// different stream views of this Controller.
53-
/// This is only exposed for test purposes. It shouldn't be used by clients of
54-
/// the plugin as it may break or change at any time.
55-
@visibleForTesting
56-
final StreamController<DeviceEvent> deviceEventStreamController =
57-
StreamController<DeviceEvent>.broadcast();
51+
late final StreamController<DeviceEvent> _deviceEventStreamController =
52+
_createDeviceEventStreamController();
53+
54+
StreamController<DeviceEvent> _createDeviceEventStreamController() {
55+
// Set up the method handler lazily.
56+
const MethodChannel channel = MethodChannel(deviceEventChannelName);
57+
channel.setMethodCallHandler(_handleDeviceMethodCall);
58+
return StreamController<DeviceEvent>.broadcast();
59+
}
5860

5961
// The stream to receive frames from the native code.
6062
StreamSubscription<dynamic>? _platformImageStreamSubscription;
@@ -192,7 +194,7 @@ class AndroidCamera extends CameraPlatform {
192194

193195
@override
194196
Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() {
195-
return deviceEventStreamController.stream
197+
return _deviceEventStreamController.stream
196198
.whereType<DeviceOrientationChangedEvent>();
197199
}
198200

@@ -518,14 +520,10 @@ class AndroidCamera extends CameraPlatform {
518520
}
519521

520522
/// Converts messages received from the native platform into device events.
521-
///
522-
/// This is only exposed for test purposes. It shouldn't be used by clients of
523-
/// the plugin as it may break or change at any time.
524-
@visibleForTesting
525-
Future<dynamic> handleDeviceMethodCall(MethodCall call) async {
523+
Future<dynamic> _handleDeviceMethodCall(MethodCall call) async {
526524
switch (call.method) {
527525
case 'orientation_changed':
528-
deviceEventStreamController.add(DeviceOrientationChangedEvent(
526+
_deviceEventStreamController.add(DeviceOrientationChangedEvent(
529527
deserializeDeviceOrientation(
530528
call.arguments['orientation']! as String)));
531529
break;

packages/camera/camera_android/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: camera_android
22
description: Android implementation of the camera plugin.
33
repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
5-
version: 0.9.8+1
5+
version: 0.9.8+2
66

77
environment:
88
sdk: ">=2.14.0 <3.0.0"

packages/camera/camera_android/test/android_camera_test.dart

+26-6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ void main() {
2525
expect(CameraPlatform.instance, isA<AndroidCamera>());
2626
});
2727

28+
test('registration does not set message handlers', () async {
29+
AndroidCamera.registerWith();
30+
31+
// Setting up a handler requires bindings to be initialized, and since
32+
// registerWith is called very early in initialization the bindings won't
33+
// have been initialized. While registerWith could intialize them, that
34+
// could slow down startup, so instead the handler should be set up lazily.
35+
final ByteData? response = await TestDefaultBinaryMessengerBinding
36+
.instance!.defaultBinaryMessenger
37+
.handlePlatformMessage(
38+
AndroidCamera.deviceEventChannelName,
39+
const StandardMethodCodec().encodeMethodCall(const MethodCall(
40+
'orientation_changed',
41+
<String, Object>{'orientation': 'portraitDown'})),
42+
(ByteData? data) {});
43+
expect(response, null);
44+
});
45+
2846
group('Creation, Initialization & Disposal Tests', () {
2947
test('Should send creation data and receive back a camera id', () async {
3048
// Arrange
@@ -402,12 +420,14 @@ void main() {
402420
// Emit test events
403421
const DeviceOrientationChangedEvent event =
404422
DeviceOrientationChangedEvent(DeviceOrientation.portraitUp);
405-
await camera.handleDeviceMethodCall(
406-
MethodCall('orientation_changed', event.toJson()));
407-
await camera.handleDeviceMethodCall(
408-
MethodCall('orientation_changed', event.toJson()));
409-
await camera.handleDeviceMethodCall(
410-
MethodCall('orientation_changed', event.toJson()));
423+
for (int i = 0; i < 3; i++) {
424+
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
425+
.handlePlatformMessage(
426+
AndroidCamera.deviceEventChannelName,
427+
const StandardMethodCodec().encodeMethodCall(
428+
MethodCall('orientation_changed', event.toJson())),
429+
null);
430+
}
411431

412432
// Assert
413433
expect(await streamQueue.next, event);

packages/camera/camera_avfoundation/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.8+2
2+
3+
* Fixes exception in registerWith caused by the switch to an in-package method channel.
4+
15
## 0.9.8+1
26

37
* Ignores deprecation warnings for upcoming styleFrom button API changes.

packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart

+18-20
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,19 @@ const MethodChannel _channel =
1919

2020
/// An iOS implementation of [CameraPlatform] based on AVFoundation.
2121
class AVFoundationCamera extends CameraPlatform {
22-
/// Construct a new method channel camera instance.
23-
AVFoundationCamera() {
24-
const MethodChannel channel =
25-
MethodChannel('plugins.flutter.io/camera_avfoundation/fromPlatform');
26-
channel.setMethodCallHandler(
27-
(MethodCall call) => handleDeviceMethodCall(call));
28-
}
29-
3022
/// Registers this class as the default instance of [CameraPlatform].
3123
static void registerWith() {
3224
CameraPlatform.instance = AVFoundationCamera();
3325
}
3426

3527
final Map<int, MethodChannel> _channels = <int, MethodChannel>{};
3628

29+
/// The name of the channel that device events from the platform side are
30+
/// sent on.
31+
@visibleForTesting
32+
static const String deviceEventChannelName =
33+
'plugins.flutter.io/camera_avfoundation/fromPlatform';
34+
3735
/// The controller we need to broadcast the different events coming
3836
/// from handleMethodCall, specific to camera events.
3937
///
@@ -50,11 +48,15 @@ class AVFoundationCamera extends CameraPlatform {
5048
///
5149
/// It is a `broadcast` because multiple controllers will connect to
5250
/// different stream views of this Controller.
53-
/// This is only exposed for test purposes. It shouldn't be used by clients of
54-
/// the plugin as it may break or change at any time.
55-
@visibleForTesting
56-
final StreamController<DeviceEvent> deviceEventStreamController =
57-
StreamController<DeviceEvent>.broadcast();
51+
late final StreamController<DeviceEvent> _deviceEventStreamController =
52+
_createDeviceEventStreamController();
53+
54+
StreamController<DeviceEvent> _createDeviceEventStreamController() {
55+
// Set up the method handler lazily.
56+
const MethodChannel channel = MethodChannel(deviceEventChannelName);
57+
channel.setMethodCallHandler(_handleDeviceMethodCall);
58+
return StreamController<DeviceEvent>.broadcast();
59+
}
5860

5961
// The stream to receive frames from the native code.
6062
StreamSubscription<dynamic>? _platformImageStreamSubscription;
@@ -192,7 +194,7 @@ class AVFoundationCamera extends CameraPlatform {
192194

193195
@override
194196
Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() {
195-
return deviceEventStreamController.stream
197+
return _deviceEventStreamController.stream
196198
.whereType<DeviceOrientationChangedEvent>();
197199
}
198200

@@ -523,14 +525,10 @@ class AVFoundationCamera extends CameraPlatform {
523525
}
524526

525527
/// Converts messages received from the native platform into device events.
526-
///
527-
/// This is only exposed for test purposes. It shouldn't be used by clients of
528-
/// the plugin as it may break or change at any time.
529-
@visibleForTesting
530-
Future<dynamic> handleDeviceMethodCall(MethodCall call) async {
528+
Future<dynamic> _handleDeviceMethodCall(MethodCall call) async {
531529
switch (call.method) {
532530
case 'orientation_changed':
533-
deviceEventStreamController.add(DeviceOrientationChangedEvent(
531+
_deviceEventStreamController.add(DeviceOrientationChangedEvent(
534532
deserializeDeviceOrientation(
535533
call.arguments['orientation']! as String)));
536534
break;

packages/camera/camera_avfoundation/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: camera_avfoundation
22
description: iOS implementation of the camera plugin.
33
repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_avfoundation
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
5-
version: 0.9.8+1
5+
version: 0.9.8+2
66

77
environment:
88
sdk: ">=2.14.0 <3.0.0"

packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart

+26-6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ void main() {
2525
expect(CameraPlatform.instance, isA<AVFoundationCamera>());
2626
});
2727

28+
test('registration does not set message handlers', () async {
29+
AVFoundationCamera.registerWith();
30+
31+
// Setting up a handler requires bindings to be initialized, and since
32+
// registerWith is called very early in initialization the bindings won't
33+
// have been initialized. While registerWith could intialize them, that
34+
// could slow down startup, so instead the handler should be set up lazily.
35+
final ByteData? response = await TestDefaultBinaryMessengerBinding
36+
.instance!.defaultBinaryMessenger
37+
.handlePlatformMessage(
38+
AVFoundationCamera.deviceEventChannelName,
39+
const StandardMethodCodec().encodeMethodCall(const MethodCall(
40+
'orientation_changed',
41+
<String, Object>{'orientation': 'portraitDown'})),
42+
(ByteData? data) {});
43+
expect(response, null);
44+
});
45+
2846
group('Creation, Initialization & Disposal Tests', () {
2947
test('Should send creation data and receive back a camera id', () async {
3048
// Arrange
@@ -402,12 +420,14 @@ void main() {
402420
// Emit test events
403421
const DeviceOrientationChangedEvent event =
404422
DeviceOrientationChangedEvent(DeviceOrientation.portraitUp);
405-
await camera.handleDeviceMethodCall(
406-
MethodCall('orientation_changed', event.toJson()));
407-
await camera.handleDeviceMethodCall(
408-
MethodCall('orientation_changed', event.toJson()));
409-
await camera.handleDeviceMethodCall(
410-
MethodCall('orientation_changed', event.toJson()));
423+
for (int i = 0; i < 3; i++) {
424+
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
425+
.handlePlatformMessage(
426+
AVFoundationCamera.deviceEventChannelName,
427+
const StandardMethodCodec().encodeMethodCall(
428+
MethodCall('orientation_changed', event.toJson())),
429+
null);
430+
}
411431

412432
// Assert
413433
expect(await streamQueue.next, event);

0 commit comments

Comments
 (0)