Unverified Commit 5257f029 authored by Daco Harkes's avatar Daco Harkes Committed by GitHub

FFI plugins (#94101)

* Building shared C source code as part of the native build for platforms Android, iOS, Linux desktop, MacOS desktop, and Windows desktop.
* Sample code doing a synchronous FFI call.
* Sample code doing a long running synchronous FFI call on a helper isolate.
* Use of `package:ffigen` to generate the bindings.
parent 85e6cea8
......@@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
......@@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST})
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
......@@ -339,7 +339,8 @@ abstract class CreateBase extends FlutterCommand {
String agpVersion,
String kotlinVersion,
String gradleVersion,
bool withPluginHook = false,
bool withPlatformChannelPluginHook = false,
bool withFfiPluginHook = false,
bool ios = false,
bool android = false,
bool web = false,
......@@ -384,7 +385,9 @@ abstract class CreateBase extends FlutterCommand {
'pluginClassCapitalSnakeCase': pluginClassCapitalSnakeCase,
'pluginDartClass': pluginDartClass,
'pluginProjectUUID': const Uuid().v4().toUpperCase(),
'withPluginHook': withPluginHook,
'withFfiPluginHook': withFfiPluginHook,
'withPlatformChannelPluginHook': withPlatformChannelPluginHook,
'withPluginHook': withFfiPluginHook || withPlatformChannelPluginHook,
'androidLanguage': androidLanguage,
'iosLanguage': iosLanguage,
'hasIosDevelopmentTeam': iosDevelopmentTeam != null && iosDevelopmentTeam.isNotEmpty,
......
......@@ -23,9 +23,14 @@ enum FlutterProjectType {
package,
/// This is a native plugin project.
plugin,
/// This is an FFI native plugin project.
ffiPlugin,
}
String flutterProjectTypeToString(FlutterProjectType type) {
if (type == FlutterProjectType.ffiPlugin) {
return 'plugin_ffi';
}
return getEnumName(type);
}
......
......@@ -47,12 +47,17 @@ class Plugin {
/// package: io.flutter.plugins.sample
/// pluginClass: SamplePlugin
/// ios:
/// # A plugin implemented through method channels.
/// pluginClass: SamplePlugin
/// linux:
/// pluginClass: SamplePlugin
/// # A plugin implemented purely in Dart code.
/// dartPluginClass: SamplePlugin
/// macos:
/// pluginClass: SamplePlugin
/// # A plugin implemented `dart:ffi`.
/// ffiPlugin: true
/// windows:
/// # A plugin using platform-specific Dart and method channels.
/// dartPluginClass: SamplePlugin
/// pluginClass: SamplePlugin
factory Plugin.fromYaml(
String name,
......
This directory contains templates for `flutter create`.
The `app_shared` subdirectory is special. It provides files for all app
templates (as opposed to plugin or module templates).
As of May 2021, there are two app templates: `app` (the counter app)
The `*_shared` subdirectories provide files for multiple templates.
* `app_shared` for `app` and `skeleton`.
* `plugin_shared` for (method channel) `plugin` and `plugin_ffi`.
For example, there are two app templates: `app` (the counter app)
and `skeleton` (the more advanced list view/detail view app).
```plain
......
import 'package:flutter/material.dart';
{{#withPluginHook}}
{{#withPlatformChannelPluginHook}}
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart';
{{/withPluginHook}}
{{/withPlatformChannelPluginHook}}
{{#withFfiPluginHook}}
import 'dart:async';
import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart' as {{pluginProjectName}};
{{/withFfiPluginHook}}
void main() {
runApp(const MyApp());
......@@ -121,7 +126,7 @@ class _MyHomePageState extends State<MyHomePage> {
}
}
{{/withPluginHook}}
{{#withPluginHook}}
{{#withPlatformChannelPluginHook}}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
......@@ -174,4 +179,71 @@ class _MyAppState extends State<MyApp> {
);
}
}
{{/withPluginHook}}
{{/withPlatformChannelPluginHook}}
{{#withFfiPluginHook}}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late int sumResult;
late Future<int> sumAsyncResult;
@override
void initState() {
super.initState();
sumResult = {{pluginProjectName}}.sum(1, 2);
sumAsyncResult = {{pluginProjectName}}.sumAsync(3, 4);
}
@override
Widget build(BuildContext context) {
const textStyle = TextStyle(fontSize: 25);
const spacerSmall = SizedBox(height: 10);
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Native Packages'),
),
body: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(10),
child: Column(
children: [
const Text(
'This calls a native function through FFI that is shipped as source in the package. '
'The native code is built as part of the Flutter Runner build.',
style: textStyle,
textAlign: TextAlign.center,
),
spacerSmall,
Text(
'sum(1, 2) = $sumResult',
style: textStyle,
textAlign: TextAlign.center,
),
spacerSmall,
FutureBuilder<int>(
future: sumAsyncResult,
builder: (BuildContext context, AsyncSnapshot<int> value) {
final displayValue =
(value.hasData) ? value.data : 'loading';
return Text(
'await sumAsync(3, 4) = $displayValue',
style: textStyle,
textAlign: TextAlign.center,
);
},
),
],
),
),
),
),
);
}
}
{{/withFfiPluginHook}}
......@@ -4,7 +4,7 @@ description: {{description}}
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
{{^withPluginHook}}
{{^withPlatformChannelPluginHook}}
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
......@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
{{/withPluginHook}}
{{/withPlatformChannelPluginHook}}
environment:
sdk: {{dartSdkVersionBounds}}
......
......@@ -8,7 +8,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
{{^withFfiPluginHook}}
import 'package:{{projectName}}/main.dart';
{{/withFfiPluginHook}}
{{^withPluginHook}}
void main() {
......@@ -30,7 +32,7 @@ void main() {
});
}
{{/withPluginHook}}
{{#withPluginHook}}
{{#withPlatformChannelPluginHook}}
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
......@@ -46,4 +48,4 @@ void main() {
);
});
}
{{/withPluginHook}}
{{/withPlatformChannelPluginHook}}
......@@ -94,11 +94,11 @@ install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
endforeach(bundled_library)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
......
import 'package:flutter/material.dart';
{{#withPluginHook}}
{{#withPlatformChannelPluginHook}}
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart';
{{/withPluginHook}}
{{/withPlatformChannelPluginHook}}
void main() => runApp(const MyApp());
{{^withPluginHook}}
{{^withPlatformChannelPluginHook}}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
......@@ -117,8 +117,8 @@ class _MyHomePageState extends State<MyHomePage> {
);
}
}
{{/withPluginHook}}
{{#withPluginHook}}
{{/withPlatformChannelPluginHook}}
{{#withPlatformChannelPluginHook}}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
......@@ -171,4 +171,4 @@ class _MyAppState extends State<MyApp> {
);
}
}
{{/withPluginHook}}
{{/withPlatformChannelPluginHook}}
......@@ -8,7 +8,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
{{^withFfiPluginHook}}
import 'package:{{projectName}}/main.dart';
{{/withFfiPluginHook}}
{{^withPluginHook}}
void main() {
......@@ -30,7 +32,7 @@ void main() {
});
}
{{/withPluginHook}}
{{#withPluginHook}}
{{#withPlatformChannelPluginHook}}
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
......@@ -46,4 +48,4 @@ void main() {
);
});
}
{{/withPluginHook}}
{{/withPlatformChannelPluginHook}}
<component name="libraryTable">
<library name="Dart SDK">
<CLASSES>
<root url="file://{{{dartSdk}}}/lib/async" />
<root url="file://{{{dartSdk}}}/lib/collection" />
<root url="file://{{{dartSdk}}}/lib/convert" />
<root url="file://{{{dartSdk}}}/lib/core" />
<root url="file://{{{dartSdk}}}/lib/developer" />
<root url="file://{{{dartSdk}}}/lib/html" />
<root url="file://{{{dartSdk}}}/lib/io" />
<root url="file://{{{dartSdk}}}/lib/isolate" />
<root url="file://{{{dartSdk}}}/lib/math" />
<root url="file://{{{dartSdk}}}/lib/mirrors" />
<root url="file://{{{dartSdk}}}/lib/typed_data" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/{{projectName}}.iml" filepath="$PROJECT_DIR$/{{projectName}}.iml" />
<module fileurl="file://$PROJECT_DIR$/android/{{projectName}}_android.iml" filepath="$PROJECT_DIR$/android/{{projectName}}_android.iml" />
<module fileurl="file://$PROJECT_DIR$/example/android/{{projectName}}_example_android.iml" filepath="$PROJECT_DIR$/example/android/{{projectName}}_example_android.iml" />
</modules>
</component>
</project>
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="example/lib/main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/example/lib/main.dart" />
<method />
</configuration>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="FileEditorManager">
<leaf>
<file leaf-file-name="{{projectName}}.dart" pinned="false" current-in-tab="true">
<entry file="file://$PROJECT_DIR$/lib/{{projectName}}.dart">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="main.dart" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/example/lib/main.dart">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
</state>
</provider>
</entry>
</file>
</leaf>
</component>
<component name="ToolWindowManager">
<editor active="true" />
<layout>
<window_info id="Project" active="true" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="0" side_tool="false" content_ui="combo" />
</layout>
</component>
<component name="ProjectView">
<navigator currentView="ProjectPane" proportions="" version="1">
</navigator>
<panes>
<pane id="ProjectPane">
<option name="show-excluded-files" value="false" />
</pane>
</panes>
</component>
<component name="PropertiesComponent">
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="dart.analysis.tool.window.force.activate" value="true" />
<property name="show.migrate.to.gradle.popup" value="false" />
</component>
</project>
......@@ -15,6 +15,6 @@ samples, guidance on mobile development, and a full API reference.
{{#no_platforms}}
The plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported.
To add platforms, run `flutter create -t plugin --platforms <platforms> .` under the same
directory. You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms.
To add platforms, run `flutter create -t plugin --platforms <platforms> .` in this directory.
You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms.
{{/no_platforms}}
......@@ -18,7 +18,7 @@ target_include_directories(${PLUGIN_NAME} INTERFACE
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter)
target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
# List of absolute paths to libraries that should be bundled with the plugin
# List of absolute paths to libraries that should be bundled with the plugin.
set({{projectName}}_bundled_libraries
""
PARENT_SCOPE
......
......@@ -17,7 +17,7 @@ target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)
# List of absolute paths to libraries that should be bundled with the plugin
# List of absolute paths to libraries that should be bundled with the plugin.
set({{projectName}}_bundled_libraries
""
PARENT_SCOPE
......
# {{projectName}}
{{description}}
## Getting Started
This project is a starting point for a Flutter
[FFI plugin](https://docs.flutter.dev/development/platform-integration/c-interop),
a specialized package that includes native code directly invoked with Dart FFI.
## Project stucture
This template uses the following structure:
* `src`: Contains the native source code, and a CmakeFile.txt file for building
that source code into a dynamic library
* `lib`: Contains the Dart code that defines the API of the plugin, and which
calls into the native code using `dart:ffi`
* platform folders (`android`, `ios`, `windows`, etc.): Contains the build files
for building and bundling the native code library with the platform application.
## Buidling and bundling native code
The `pubspec.yaml` specifies FFI plugins as follows.
```yaml
plugin:
platforms:
some_platform:
ffiPlugin: true
```
This configuration invokes the native build for the various target platforms
and bundles the binaries in Flutter applications using these FFI plugins.
A plugin can have both FFI and method channels:
```yaml
plugin:
platforms:
some_platform:
pluginClass: SomeName
ffiPlugin: true
```
The native build systems that are invoked by FFI (and method channel) plugins are:
* For Android: Gradle, which invokes the Android NDK for native builds.
* See the documentation in android/build.gradle.
* For iOS and MacOS: Xcode, via CocoaPods.
* See the documentation in ios/{{projectName}}.podspec.
* See the documentation in macos/{{projectName}}.podspec.
* For Linux and Windows: CMake.
* See the documentation in linux/CMakeLists.txt.
* See the documentation in windows/CMakeLists.txt.
## Binding to native code
To use the native code, bindings in Dart are needed.
To avoid writing these by hand, they are generated from the header file
(`src/{{projectName}}.h`) by `package:ffigen`.
Regenerate the bindings by running `flutter pub run ffigen --config ffigen.yaml`.
## Invoking native code
Very short-running native functions can be directly invoked from any isolate.
For example, see `sum` in `lib/{{projectName}}.dart`.
Longer-running functions should be invoked on a helper isolate to avoid
dropping frames in Flutter applications.
For example, see `sumAsync` in `lib/{{projectName}}.dart`.
## Flutter help
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
{{#no_platforms}}
The plugin project was generated without specifying the `--platforms` flag, so no platforms are currently supported.
To add platforms, run `flutter create -t plugin_ffi --platforms <platforms> .` in this directory.
You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms.
{{/no_platforms}}
// The Android Gradle Plugin builds the native code with the Android NDK.
group '{{androidIdentifier}}'
version '1.0'
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
// The Android Gradle Plugin knows how to build native code with the NDK.
classpath 'com.android.tools.build:gradle:4.1.0'
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 31
// Invoke the shared CMake build with the Android Gradle Plugin.
//
// We do _not_ specify the NDK and CMake version here. This way we will
// automatically get the default versions from the Android Gradle Plugin.
//
// Android Gradle Plugin 4.1.0: Android NDK 21.1.6352462.
// https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp
// CMake 3.10.2.
// https://developer.android.com/studio/projects/install-ndk#vanilla_cmake
//
// You are strongly encouraged to not ship FFI plugins requiring newer
// versions of the NDK or CMake, because your clients will not be able to
// use them.
externalNativeBuild {
cmake {
path "../src/CMakeLists.txt"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 16
}
}
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android" name="Android">
<configuration>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="GEN_FOLDER_RELATIVE_PATH_APT" value="/gen" />
<option name="GEN_FOLDER_RELATIVE_PATH_AIDL" value="/gen" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
<option name="LIBS_FOLDER_RELATIVE_PATH" value="/src/main/libs" />
<option name="PROGUARD_LOGS_FOLDER_RELATIVE_PATH" value="/src/main/proguard_logs" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
</content>
<orderEntry type="jdk" jdkName="Android API {{androidSdkVersion}} Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Flutter for Android" level="project" />
</component>
</module>
# Run with `dart run ffigen --config ffigen.yaml`.
name: {{pluginDartClass}}Bindings
description: |
Bindings for `src/{{projectName}}.h`.
Regenerate bindings with `dart run ffigen --config ffigen.yaml`.
output: 'lib/{{projectName}}_bindings_generated.dart'
headers:
entry-points:
- 'src/{{projectName}}.h'
include-directives:
- 'src/{{projectName}}.h'
preamble: |
// ignore_for_file: always_specify_types
// ignore_for_file: camel_case_types
// ignore_for_file: non_constant_identifier_names
comments:
style: any
length: full
// Relative import to be able to reuse the C sources.
#include "../../src/{{projectName}}.c"
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint {{projectName}}.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = '{{projectName}}'
s.version = '0.0.1'
s.summary = '{{description}}'
s.description = <<-DESC
{{description}}
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
# This will ensure the source files in Classes/ are included in the native
# builds of apps using this FFI plugin. Podspec does not support relative
# paths, so Classes contains a forwarder C file that relatively imports
# `../src/*` so that the C sources can be shared among all target platforms.
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '9.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end
{{#no_platforms}}
// You have generated a new plugin project without specifying the `--platforms`
// flag. An FFI plugin project that supports no platforms is generated.
// To add platforms, run `flutter create -t plugin_ffi --platforms <platforms> .`
// in this directory. You can also find a detailed instruction on how to
// add platforms in the `pubspec.yaml` at
// https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms.
{{/no_platforms}}
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import '{{projectName}}_bindings_generated.dart';
/// A very short-lived native function.
///
/// For very short-lived functions, it is fine to call them on the main isolate.
/// They will block the Dart execution while running the native function, so
/// only do this for native functions which are guaranteed to be short-lived.
int sum(int a, int b) => _bindings.sum(a, b);
/// A longer lived native function, which occupies the thread calling it.
///
/// Do not call these kind of native functions in the main isolate. They will
/// block Dart execution. This will cause dropped frames in Flutter applications.
/// Instead, call these native functions on a separate isolate.
///
/// Modify this to suit your own use case. Example use cases:
///
/// 1. Reuse a single isolate for various different kinds of requests.
/// 2. Use multiple helper isolates for parallel execution.
Future<int> sumAsync(int a, int b) async {
final SendPort helperIsolateSendPort = await _helperIsolateSendPort;
final int requestId = _nextSumRequestId++;
final _SumRequest request = _SumRequest(requestId, a, b);
final Completer<int> completer = Completer<int>();
_sumRequests[requestId] = completer;
helperIsolateSendPort.send(request);
return completer.future;
}
const String _libName = '{{projectName}}';
/// The dynamic library in which the symbols for [{{pluginDartClass}}Bindings] can be found.
final DynamicLibrary _dylib = () {
if (Platform.isMacOS || Platform.isIOS) {
return DynamicLibrary.open('$_libName.framework/$_libName');
}
if (Platform.isAndroid || Platform.isLinux) {
return DynamicLibrary.open('lib$_libName.so');
}
if (Platform.isWindows) {
return DynamicLibrary.open('$_libName.dll');
}
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();
/// The bindings to the native functions in [_dylib].
final {{pluginDartClass}}Bindings _bindings = {{pluginDartClass}}Bindings(_dylib);
/// A request to compute `sum`.
///
/// Typically sent from one isolate to another.
class _SumRequest {
final int id;
final int a;
final int b;
const _SumRequest(this.id, this.a, this.b);
}
/// A response with the result of `sum`.
///
/// Typically sent from one isolate to another.
class _SumResponse {
final int id;
final int result;
const _SumResponse(this.id, this.result);
}
/// Counter to identify [_SumRequest]s and [_SumResponse]s.
int _nextSumRequestId = 0;
/// Mapping from [_SumRequest] `id`s to the completers corresponding to the correct future of the pending request.
final Map<int, Completer<int>> _sumRequests = <int, Completer<int>>{};
/// The SendPort belonging to the helper isolate.
Future<SendPort> _helperIsolateSendPort = () async {
// The helper isolate is going to send us back a SendPort, which we want to
// wait for.
final Completer<SendPort> completer = Completer<SendPort>();
// Receive port on the main isolate to receive messages from the helper.
// We receive two types of messages:
// 1. A port to send messages on.
// 2. Responses to requests we sent.
final ReceivePort receivePort = ReceivePort()
..listen((dynamic data) {
if (data is SendPort) {
// The helper isolate sent us the port on which we can sent it requests.
completer.complete(data);
return;
}
if (data is _SumResponse) {
// The helper isolate sent us a response to a request we sent.
final Completer<int> completer = _sumRequests[data.id]!;
_sumRequests.remove(data.id);
completer.complete(data.result);
return;
}
throw UnsupportedError('Unsupported message type: ${data.runtimeType}');
});
// Start the helper isolate.
await Isolate.spawn((SendPort sendPort) async {
final ReceivePort helperReceivePort = ReceivePort()
..listen((dynamic data) {
// On the helper isolate listen to requests and respond to them.
if (data is _SumRequest) {
final int result = _bindings.sum_long_running(data.a, data.b);
final _SumResponse response = _SumResponse(data.id, result);
sendPort.send(response);
return;
}
throw UnsupportedError('Unsupported message type: ${data.runtimeType}');
});
// Send the the port to the main isolate on which we can receive requests.
sendPort.send(helperReceivePort.sendPort);
}, receivePort.sendPort);
// Wait until the helper isolate has sent us back the SendPort on which we
// can start sending requests.
return completer.future;
}();
// ignore_for_file: always_specify_types
// ignore_for_file: camel_case_types
// ignore_for_file: non_constant_identifier_names
// AUTO GENERATED FILE, DO NOT EDIT.
//
// Generated by `package:ffigen`.
import 'dart:ffi' as ffi;
/// Bindings for `src/{{projectName}}.h`.
///
/// Regenerate bindings with `dart run ffigen --config ffigen.yaml`.
///
class {{pluginDartClass}}Bindings {
/// Holds the symbol lookup function.
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
_lookup;
/// The symbols are looked up in [dynamicLibrary].
{{pluginDartClass}}Bindings(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
/// The symbols are looked up with [lookup].
{{pluginDartClass}}Bindings.fromLookup(
ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
lookup)
: _lookup = lookup;
/// A very short-lived native function.
///
/// For very short-lived functions, it is fine to call them on the main isolate.
/// They will block the Dart execution while running the native function, so
/// only do this for native functions which are guaranteed to be short-lived.
int sum(
int a,
int b,
) {
return _sum(
a,
b,
);
}
late final _sumPtr =
_lookup<ffi.NativeFunction<ffi.IntPtr Function(ffi.IntPtr, ffi.IntPtr)>>(
'sum');
late final _sum = _sumPtr.asFunction<int Function(int, int)>();
/// A longer lived native function, which occupies the thread calling it.
///
/// Calling these kind of native functions in the main isolate will
/// block Dart execution and cause dropped frames in Flutter applications.
/// Consider calling such native functions from a separate isolate.
int sum_long_running(
int a,
int b,
) {
return _sum_long_running(
a,
b,
);
}
late final _sum_long_runningPtr =
_lookup<ffi.NativeFunction<ffi.IntPtr Function(ffi.IntPtr, ffi.IntPtr)>>(
'sum_long_running');
late final _sum_long_running =
_sum_long_runningPtr.asFunction<int Function(int, int)>();
}
cmake_minimum_required(VERSION 3.10)
set(PROJECT_NAME "{{projectName}}")
project(${PROJECT_NAME} LANGUAGES CXX)
# Invoke the build for native code shared with the other target platforms.
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_BINARY_DIR}/shared")
# List of absolute paths to libraries that should be bundled with the plugin.
set({{projectName}}_bundled_libraries
# Defined in ../src/CMakeLists.txt.
$<TARGET_FILE:{{projectName}}>
PARENT_SCOPE
)
// Relative import to be able to reuse the C sources.
#include "../../src/{{projectName}}.c"
# We use the CMake version connected to the version of the Android Gradle Plugin.
#
# Android Gradle Plugin 4.1.0: Android NDK 21.1.6352462.
# https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp
# CMake 3.10.2.
# https://developer.android.com/studio/projects/install-ndk#vanilla_cmake
#
# You are strongly encouraged to not ship FFI plugins requiring newer versions
# of the NDK or CMake, because your clients will not be able to use them.
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)
project({{projectName}}_library VERSION 0.0.1 LANGUAGES C)
add_library(
{{projectName}}
SHARED
"{{projectName}}.c"
)
set_target_properties({{projectName}} PROPERTIES
PUBLIC_HEADER {{projectName}}.h
OUTPUT_NAME "{{projectName}}"
)
target_compile_definitions({{projectName}} PUBLIC DART_SHARED_LIB)
#include "{{projectName}}.h"
// A very short-lived native function.
//
// For very short-lived functions, it is fine to call them on the main isolate.
// They will block the Dart execution while running the native function, so
// only do this for native functions which are guaranteed to be short-lived.
MYLIB_EXPORT intptr_t sum(intptr_t a, intptr_t b) { return a + b; }
// A longer-lived native function, which occupies the thread calling it.
//
// Do not call these kind of native functions in the main isolate. They will
// block Dart execution. This will cause dropped frames in Flutter applications.
// Instead, call these native functions on a separate isolate.
MYLIB_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b) {
// Simulate work.
#if _WIN32
Sleep(5000);
#else
usleep(5000 * 1000);
#endif
return a + b;
}
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#if _WIN32
#include <windows.h>
#else
#include <pthread.h>
#include <unistd.h>
#endif
#if _WIN32
#define MYLIB_EXPORT __declspec(dllexport)
#else
#define MYLIB_EXPORT
#endif
// A very short-lived native function.
//
// For very short-lived functions, it is fine to call them on the main isolate.
// They will block the Dart execution while running the native function, so
// only do this for native functions which are guaranteed to be short-lived.
MYLIB_EXPORT intptr_t sum(intptr_t a, intptr_t b);
// A longer lived native function, which occupies the thread calling it.
//
// Do not call these kind of native functions in the main isolate. They will
// block Dart execution. This will cause dropped frames in Flutter applications.
// Instead, call these native functions on a separate isolate.
MYLIB_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b);
cmake_minimum_required(VERSION 3.14)
set(PROJECT_NAME "{{projectName}}")
project(${PROJECT_NAME} LANGUAGES CXX)
# Invoke the build for native code shared with the other target platforms.
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_BINARY_DIR}/shared")
# List of absolute paths to libraries that should be bundled with the plugin.
set({{projectName}}_bundled_libraries
# Defined in ../src/CMakeLists.txt.
$<TARGET_FILE:{{projectName}}>
PARENT_SCOPE
)
......@@ -7,4 +7,9 @@ version:
revision: {{flutterRevision}}
channel: {{flutterChannel}}
{{#withFfiPluginHook}}
project_type: plugin_ffi
{{/withFfiPluginHook}}
{{#withPlatformChannelPluginHook}}
project_type: plugin
{{/withPlatformChannelPluginHook}}
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="{{androidIdentifier}}">
</manifest>
......@@ -12,6 +12,13 @@ Pod::Spec.new do |s|
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
{{#withFfiPluginHook}}
# This will ensure the source files in Classes/ are included in the native
# builds of apps using this FFI plugin. Podspec does not support relative
# paths, so Classes contains a forwarder C file that relatively imports
# `../src/*` so that the C sources can be shared among all target platforms.
{{/withFfiPluginHook}}
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'FlutterMacOS'
......
......@@ -16,4 +16,4 @@
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
</component>
</module>
\ No newline at end of file
</module>
......@@ -16,6 +16,9 @@ dependencies:
{{/web}}
dev_dependencies:
{{#withFfiPluginHook}}
ffigen: ^4.1.2
{{/withFfiPluginHook}}
flutter_test:
sdk: flutter
flutter_lints: ^1.0.0
......@@ -26,9 +29,52 @@ dev_dependencies:
# The following section is specific to Flutter.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' and Android 'package' identifiers should not ordinarily
# be modified. They are used by the tooling to maintain consistency when
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, ObjectiveC)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
{{#withFfiPluginHook}}
#
# Please refer to README.md for a detailed explanation.
plugin:
platforms:
{{#no_platforms}}
# This FFI plugin project was generated without specifying any
# platforms with the `--platform` argument. If you see the `some_platform` map below, remove it and
# then add platforms following the instruction here:
# https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms
# -------------------
some_platform:
ffiPlugin: true
# -------------------
{{/no_platforms}}
{{#android}}
android:
ffiPlugin: true
{{/android}}
{{#ios}}
ios:
ffiPlugin: true
{{/ios}}
{{#linux}}
linux:
ffiPlugin: true
{{/linux}}
{{#macos}}
macos:
ffiPlugin: true
{{/macos}}
{{#windows}}
windows:
ffiPlugin: true
{{/windows}}
{{/withFfiPluginHook}}
{{#withPlatformChannelPluginHook}}
plugin:
platforms:
{{#no_platforms}}
......@@ -67,6 +113,7 @@ flutter:
pluginClass: {{pluginDartClass}}Web
fileName: {{projectName}}_web.dart
{{/web}}
{{/withPlatformChannelPluginHook}}
# To add assets to your plugin package, add an assets section, like this:
# assets:
......
......@@ -304,13 +304,6 @@
"templates/package/README.md.tmpl",
"templates/package/test/projectName_test.dart.tmpl",
"templates/plugin/.gitignore.tmpl",
"templates/plugin/.idea/libraries/Dart_SDK.xml.tmpl",
"templates/plugin/.idea/modules.xml.tmpl",
"templates/plugin/.idea/runConfigurations/example_lib_main_dart.xml.tmpl",
"templates/plugin/.idea/workspace.xml.tmpl",
"templates/plugin/.metadata.tmpl",
"templates/plugin/analysis_options.yaml.tmpl",
"templates/plugin/android-java.tmpl/build.gradle.tmpl",
"templates/plugin/android-java.tmpl/projectName_android.iml.tmpl",
"templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl",
......@@ -322,7 +315,6 @@
"templates/plugin/android.tmpl/gradle.properties.tmpl",
"templates/plugin/android.tmpl/settings.gradle.tmpl",
"templates/plugin/android.tmpl/src/main/AndroidManifest.xml.tmpl",
"templates/plugin/CHANGELOG.md.tmpl",
"templates/plugin/ios-objc.tmpl/Classes/pluginClass.h.tmpl",
"templates/plugin/ios-objc.tmpl/Classes/pluginClass.m.tmpl",
"templates/plugin/ios-objc.tmpl/projectName.podspec.tmpl",
......@@ -333,22 +325,51 @@
"templates/plugin/ios.tmpl/.gitignore",
"templates/plugin/ios.tmpl/Assets/.gitkeep",
"templates/plugin/lib/projectName.dart.tmpl",
"templates/plugin/LICENSE.tmpl",
"templates/plugin/linux.tmpl/CMakeLists.txt.tmpl",
"templates/plugin/linux.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl",
"templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl",
"templates/plugin/macos.tmpl/Classes/pluginClass.swift.tmpl",
"templates/plugin/macos.tmpl/projectName.podspec.tmpl",
"templates/plugin/projectName.iml.tmpl",
"templates/plugin/pubspec.yaml.tmpl",
"templates/plugin/README.md.tmpl",
"templates/plugin/test/projectName_test.dart.tmpl",
"templates/plugin/windows.tmpl/.gitignore",
"templates/plugin/windows.tmpl/CMakeLists.txt.tmpl",
"templates/plugin/windows.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl",
"templates/plugin/windows.tmpl/pluginClassSnakeCase.cpp.tmpl",
"templates/plugin/lib/projectName_web.dart.tmpl",
"templates/plugin_ffi/android.tmpl/build.gradle.tmpl",
"templates/plugin_ffi/android.tmpl/projectName_android.iml.tmpl",
"templates/plugin_ffi/ffigen.yaml.tmpl",
"templates/plugin_ffi/ios.tmpl/.gitignore",
"templates/plugin_ffi/ios.tmpl/Classes/projectName.c.tmpl",
"templates/plugin_ffi/ios.tmpl/projectName.podspec.tmpl",
"templates/plugin_ffi/lib/projectName_bindings_generated.dart.tmpl",
"templates/plugin_ffi/lib/projectName.dart.tmpl",
"templates/plugin_ffi/linux.tmpl/CMakeLists.txt.tmpl",
"templates/plugin_ffi/linux.tmpl/include/projectName.tmpl/plugin_ffiClassSnakeCase.h.tmpl",
"templates/plugin_ffi/macos.tmpl/Classes/projectName.c.tmpl",
"templates/plugin_ffi/README.md.tmpl",
"templates/plugin_ffi/src.tmpl/CMakeLists.txt.tmpl",
"templates/plugin_ffi/src.tmpl/projectName.c.tmpl",
"templates/plugin_ffi/src.tmpl/projectName.h.tmpl",
"templates/plugin_ffi/windows.tmpl/CMakeLists.txt.tmpl",
"templates/plugin_shared/.gitignore.tmpl",
"templates/plugin_shared/.idea/libraries/Dart_SDK.xml.tmpl",
"templates/plugin_shared/.idea/modules.xml.tmpl",
"templates/plugin_shared/.idea/runConfigurations/example_lib_main_dart.xml.tmpl",
"templates/plugin_shared/.idea/workspace.xml.tmpl",
"templates/plugin_shared/.metadata.tmpl",
"templates/plugin_shared/analysis_options.yaml.tmpl",
"templates/plugin_shared/android.tmpl/.gitignore",
"templates/plugin_shared/android.tmpl/settings.gradle.tmpl",
"templates/plugin_shared/android.tmpl/src/main/AndroidManifest.xml.tmpl",
"templates/plugin_shared/CHANGELOG.md.tmpl",
"templates/plugin_shared/LICENSE.tmpl",
"templates/plugin_shared/macos.tmpl/projectName.podspec.tmpl",
"templates/plugin_shared/projectName.iml.tmpl",
"templates/plugin_shared/pubspec.yaml.tmpl",
"templates/plugin_shared/windows.tmpl/.gitignore",
"templates/skeleton/assets/images/2.0x/flutter_logo.png.img.tmpl",
"templates/skeleton/assets/images/3.0x/flutter_logo.png.img.tmpl",
"templates/skeleton/assets/images/flutter_logo.png.img.tmpl",
......
......@@ -48,6 +48,8 @@ void main() {
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'module', 'common'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'package'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_ffi'),
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'plugin_shared'),
];
for (final String templatePath in templatePaths) {
globals.fs.directory(templatePath).createSync(recursive: true);
......@@ -96,6 +98,9 @@ void main() {
await runner.run(<String>['create', '--no-pub', '--template=plugin', 'testy']);
expect((await command.usageValues).commandCreateProjectType, 'plugin');
await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', 'testy']);
expect((await command.usageValues).commandCreateProjectType, 'plugin_ffi');
}));
testUsingContext('set iOS host language type as usage value', () => testbed.run(() async {
......
......@@ -2761,6 +2761,71 @@ void main() {
platform: globals.platform,
),
});
testUsingContext('create an FFI plugin with ios, then add macos', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', '--platform=ios', projectDir.path]);
expect(projectDir.childDirectory('src'), exists);
expect(projectDir.childDirectory('ios'), exists);
expect(projectDir.childDirectory('example').childDirectory('ios'), exists);
validatePubspecForPlugin(
projectDir: projectDir.absolute.path,
expectedPlatforms: const <String>[
'ios',
],
ffiPlugin: true,
unexpectedPlatforms: <String>['some_platform'],
);
await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', '--platform=macos', projectDir.path]);
expect(projectDir.childDirectory('macos'), exists);
expect(
projectDir.childDirectory('example').childDirectory('macos'), exists);
expect(projectDir.childDirectory('ios'), exists);
expect(projectDir.childDirectory('example').childDirectory('ios'), exists);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
});
testUsingContext('FFI plugins error android and ios language', () async {
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
final List<String> args = <String>[
'create',
'--no-pub',
'--template=plugin_ffi',
'-a',
'kotlin',
'--ios-language',
'swift',
'--platforms=ios,android',
projectDir.path,
];
await expectLater(
runner.run(args),
throwsToolExit(
message:
'The "ios-language" option is not supported with the plugin_ffi template: the language will always be C or C++.'));
});
testUsingContext('should show warning when disabled platforms are selected while creating an FFI plugin', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', '--platforms=android,ios,windows,macos,linux', projectDir.path]);
await runner.run(<String>['create', '--no-pub', '--template=plugin_ffi', projectDir.path]);
expect(logger.statusText, contains(_kDisabledPlatformRequestedMessage));
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(),
Logger: () => logger,
});
}
Future<void> _createProject(
......
......@@ -9,33 +9,43 @@ import '../src/common.dart';
import 'test_utils.dart';
void main() {
late Directory tempDir;
late Directory tempDirPluginMethodChannels;
late Directory tempDirPluginFfi;
setUp(() async {
tempDir = createResolvedTempDirectorySync('flutter_plugin_test.');
tempDirPluginMethodChannels = createResolvedTempDirectorySync('flutter_plugin_test.');
tempDirPluginFfi =
createResolvedTempDirectorySync('flutter_ffi_plugin_test.');
});
tearDown(() async {
tryToDelete(tempDir);
tryToDelete(tempDirPluginMethodChannels);
tryToDelete(tempDirPluginFfi);
});
test('plugin example can be built using current Flutter Gradle plugin', () async {
Future<void> testPlugin({
required String template,
required Directory tempDir,
}) async {
final String flutterBin = fileSystem.path.join(
getFlutterRoot(),
'bin',
'flutter',
);
final String testName = '${template}_test';
processManager.runSync(<String>[
flutterBin,
...getLocalEngineArguments(),
'create',
'--template=plugin',
'--template=$template',
'--platforms=android',
'plugin_test',
testName,
], workingDirectory: tempDir.path);
final Directory exampleAppDir = tempDir.childDirectory('plugin_test').childDirectory('example');
final Directory exampleAppDir =
tempDir.childDirectory(testName).childDirectory('example');
final File buildGradleFile = exampleAppDir.childDirectory('android').childFile('build.gradle');
expect(buildGradleFile, exists);
......@@ -68,6 +78,11 @@ void main() {
));
expect(exampleApk, exists);
if (template == 'plugin_ffi') {
// Does not support AGP 3.3.0.
return;
}
// Clean
processManager.runSync(<String>[
flutterBin,
......@@ -101,5 +116,21 @@ android.enableR8=true''');
'--target-platform=android-arm',
], workingDirectory: exampleAppDir.path);
expect(exampleApk, exists);
}
test('plugin example can be built using current Flutter Gradle plugin',
() async {
await testPlugin(
template: 'plugin',
tempDir: tempDirPluginMethodChannels,
);
});
test('FFI plugin example can be built using current Flutter Gradle plugin',
() async {
await testPlugin(
template: 'plugin_ffi',
tempDir: tempDirPluginFfi,
);
});
}
......@@ -11,19 +11,23 @@ import 'common.dart';
/// Check if the pubspec.yaml file under the `projectDir` is valid for a plugin project.
void validatePubspecForPlugin({
required String projectDir,
required String pluginClass,
String? pluginClass,
bool ffiPlugin = false,
required List<String> expectedPlatforms,
List<String> unexpectedPlatforms = const <String>[],
String? androidIdentifier,
String? webFileName,
}) {
assert(pluginClass != null || ffiPlugin);
final FlutterManifest manifest =
FlutterManifest.createFromPath('$projectDir/pubspec.yaml', fileSystem: globals.fs, logger: globals.logger)!;
final YamlMap platformMaps = YamlMap.wrap(manifest.supportedPlatforms!);
for (final String platform in expectedPlatforms) {
expect(platformMaps[platform], isNotNull);
final YamlMap platformMap = platformMaps[platform]! as YamlMap;
expect(platformMap['pluginClass'], pluginClass);
if (pluginClass != null) {
expect(platformMap['pluginClass'], pluginClass);
}
if (platform == 'android') {
expect(platformMap['package'], androidIdentifier);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment