Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [android] Bound the IrisMethodChannel lifecycle with the FlutterEngine #75

Merged
merged 8 commits into from
Aug 10, 2023
6 changes: 3 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -197,17 +197,17 @@ jobs:
integration_test_ios:
name: ios integration test
if: ${{ !contains(github.event.pull_request.labels.*.name, 'ci:skip') }}
runs-on: macos-12
runs-on: macos-13
timeout-minutes: 60
steps:
- uses: actions/checkout@v1
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.10.0' # Run the latest version
cache: true
- uses: futureware-tech/simulator-action@v1
- uses: futureware-tech/simulator-action@v2
with:
model: 'iPhone 13 Pro Max'
model: 'iPhone 14 Pro Max'
- name: ios integration test
run: |
flutter packages get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,13 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBindin

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else {
result.notImplemented();
}
result.notImplemented();
}

@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
// Notify the dart side to call the `IrisMethodChannel.dispose` to clear the resources.
channel.invokeMethod("onDetachedFromEngine_fromPlatform", null);
channel.setMethodCallHandler(null);
}
}
21 changes: 19 additions & 2 deletions lib/src/iris_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import 'dart:typed_data';
import 'package:async/async.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart'
show SynchronousFuture, VoidCallback, visibleForTesting;
show SynchronousFuture, VoidCallback, debugPrint, visibleForTesting;
import 'package:flutter/services.dart' show MethodChannel;
import 'package:iris_method_channel/src/bindings/native_iris_api_common_bindings.dart'
as iris;
import 'package:iris_method_channel/src/iris_event.dart';
Expand Down Expand Up @@ -139,7 +140,7 @@ class _Messenger implements DisposableObject {

@override
Future<void> dispose() async {
if (!_isDisposed) {
if (_isDisposed) {
return;
}
_isDisposed = true;
Expand Down Expand Up @@ -309,6 +310,8 @@ class IrisMethodChannel {
late Isolate workerIsolate;
late _HotRestartFinalizer _hotRestartFinalizer;

final MethodChannel _channel = const MethodChannel('iris_method_channel');

static Future<void> _execute(_InitilizationArgs args) async {
final SendPort mainApiCallSendPort = args.apiCallPortSendPort;
final SendPort mainEventSendPort = args.eventPortSendPort;
Expand Down Expand Up @@ -401,6 +404,18 @@ class IrisMethodChannel {
Isolate.exit(onExitSendPort, 0);
}

void _setuponDetachedFromEngineListener() {
_channel.setMethodCallHandler((call) async {
if (call.method == 'onDetachedFromEngine_fromPlatform') {
debugPrint('Receive the onDetachedFromEngine callback, clean the native resources.');
dispose();
return true;
}

return false;
});
}

Future<InitilizationResult?> initilize(List<int> args) async {
if (_initilized) {
return null;
Expand All @@ -409,6 +424,8 @@ class IrisMethodChannel {
final apiCallPort = ReceivePort();
final eventPort = ReceivePort();

_setuponDetachedFromEngineListener();

_hotRestartFinalizer = _HotRestartFinalizer(_nativeBindingsProvider);

workerIsolate = await Isolate.spawn(
Expand Down
60 changes: 59 additions & 1 deletion test/iris_method_channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import 'dart:isolate';
import 'dart:typed_data';

import 'package:ffi/ffi.dart';
import 'package:flutter/services.dart' show StandardMethodCodec, MethodCall;
import 'package:flutter_test/flutter_test.dart';
import 'package:iris_method_channel/src/bindings/native_iris_api_common_bindings.dart';
import 'package:iris_method_channel/src/bindings/native_iris_event_bindings.dart'
as iris_event;
import 'package:iris_method_channel/src/iris_event.dart';
import 'package:iris_method_channel/src/iris_method_channel.dart';
import 'package:iris_method_channel/src/native_bindings_delegate.dart';
import 'package:iris_method_channel/src/scoped_objects.dart';
import 'package:test/test.dart';

class _ApiParam {
_ApiParam(this.event, this.data);
Expand Down Expand Up @@ -227,6 +228,8 @@ class _TestEventLoopEventHandler extends EventLoopEventHandler {
}

void main() {
final binding = TestWidgetsFlutterBinding.ensureInitialized();

late _FakeNativeBindingDelegateMessenger messenger;
late NativeBindingsProvider nativeBindingsProvider;
late IrisMethodChannel irisMethodChannel;
Expand Down Expand Up @@ -416,6 +419,61 @@ void main() {
await irisMethodChannel.dispose();
});

test('disposed', () async {
await irisMethodChannel.initilize([]);
await irisMethodChannel.dispose();
// Wait for `dispose` completed.
await Future.delayed(const Duration(milliseconds: 500));
final callRecord1 = messenger.callApiRecords
.where((e) => e.methodCall.funcName == 'destroyNativeApiEngine');
expect(callRecord1.length, 1);

final callRecord2 = messenger.callApiRecords
.where((e) => e.methodCall.funcName == 'destroyIrisEventHandler');
expect(callRecord2.length, 1);
});

test('disposed multiple times', () async {
await irisMethodChannel.initilize([]);
await irisMethodChannel.dispose();
await irisMethodChannel.dispose();
// Wait for `dispose` completed.
await Future.delayed(const Duration(milliseconds: 500));
final callRecord1 = messenger.callApiRecords
.where((e) => e.methodCall.funcName == 'destroyNativeApiEngine');
expect(callRecord1.length, 1);

final callRecord2 = messenger.callApiRecords
.where((e) => e.methodCall.funcName == 'destroyIrisEventHandler');
expect(callRecord2.length, 1);
});

test('disposed after receive onDetachedFromEngine_fromPlatform', () async {
await irisMethodChannel.initilize([]);

// Simulate the `MethodChannel` call from native side
const StandardMethodCodec codec = StandardMethodCodec();
final ByteData data = codec.encodeMethodCall(const MethodCall(
'onDetachedFromEngine_fromPlatform',
));
await binding.defaultBinaryMessenger.handlePlatformMessage(
'iris_method_channel',
data,
(ByteData? data) {},
);

// Wait for the `iris_method_channel` method channel call completed.
await Future.delayed(const Duration(milliseconds: 1000));

final callRecord1 = messenger.callApiRecords
.where((e) => e.methodCall.funcName == 'destroyNativeApiEngine');
expect(callRecord1.length, 1);

final callRecord2 = messenger.callApiRecords
.where((e) => e.methodCall.funcName == 'destroyIrisEventHandler');
expect(callRecord2.length, 1);
});

test('invokeMethod after disposed', () async {
await irisMethodChannel.initilize([]);
await irisMethodChannel.dispose();
Expand Down
Loading