Skip to content

Commit

Permalink
fix: [android] Bound the IrisMethodChannel lifecycle with the Flutter…
Browse files Browse the repository at this point in the history
…Engine (#75)

* fix: [android] Bound the IrisMethodChannel lifecycle with the FlutterEngine
  • Loading branch information
littleGnAl authored Aug 10, 2023
1 parent 2d3c549 commit 7249bf0
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 11 deletions.
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

0 comments on commit 7249bf0

Please sign in to comment.