Unverified Commit 0aab2280 authored by Jeff Ward's avatar Jeff Ward Committed by GitHub

First pass at using platform abstraction for plugins (#92672)

parent e8af40f0
...@@ -31,7 +31,7 @@ TaskFunction dartPluginRegistryTest({ ...@@ -31,7 +31,7 @@ TaskFunction dartPluginRegistryTest({
'io.flutter.devicelab', 'io.flutter.devicelab',
'--platforms', '--platforms',
'macos', 'macos',
'plugin_platform_implementation', 'aplugin_platform_implementation',
], ],
environment: environment, environment: environment,
); );
...@@ -39,9 +39,9 @@ TaskFunction dartPluginRegistryTest({ ...@@ -39,9 +39,9 @@ TaskFunction dartPluginRegistryTest({
final File pluginMain = File(path.join( final File pluginMain = File(path.join(
tempDir.absolute.path, tempDir.absolute.path,
'plugin_platform_implementation', 'aplugin_platform_implementation',
'lib', 'lib',
'plugin_platform_implementation.dart', 'aplugin_platform_implementation.dart',
)); ));
if (!pluginMain.existsSync()) { if (!pluginMain.existsSync()) {
return TaskResult.failure('${pluginMain.path} does not exist'); return TaskResult.failure('${pluginMain.path} does not exist');
...@@ -49,9 +49,9 @@ TaskFunction dartPluginRegistryTest({ ...@@ -49,9 +49,9 @@ TaskFunction dartPluginRegistryTest({
// Patch plugin main dart file. // Patch plugin main dart file.
await pluginMain.writeAsString(''' await pluginMain.writeAsString('''
class PluginPlatformInterfaceMacOS { class ApluginPlatformInterfaceMacOS {
static void registerWith() { static void registerWith() {
print('PluginPlatformInterfaceMacOS.registerWith() was called'); print('ApluginPlatformInterfaceMacOS.registerWith() was called');
} }
} }
''', flush: true); ''', flush: true);
...@@ -59,18 +59,18 @@ class PluginPlatformInterfaceMacOS { ...@@ -59,18 +59,18 @@ class PluginPlatformInterfaceMacOS {
// Patch plugin main pubspec file. // Patch plugin main pubspec file.
final File pluginImplPubspec = File(path.join( final File pluginImplPubspec = File(path.join(
tempDir.absolute.path, tempDir.absolute.path,
'plugin_platform_implementation', 'aplugin_platform_implementation',
'pubspec.yaml', 'pubspec.yaml',
)); ));
String pluginImplPubspecContent = await pluginImplPubspec.readAsString(); String pluginImplPubspecContent = await pluginImplPubspec.readAsString();
pluginImplPubspecContent = pluginImplPubspecContent.replaceFirst( pluginImplPubspecContent = pluginImplPubspecContent.replaceFirst(
' pluginClass: PluginPlatformImplementationPlugin', ' pluginClass: ApluginPlatformImplementationPlugin',
' pluginClass: PluginPlatformImplementationPlugin\n' ' pluginClass: ApluginPlatformImplementationPlugin\n'
' dartPluginClass: PluginPlatformInterfaceMacOS\n', ' dartPluginClass: ApluginPlatformInterfaceMacOS\n',
); );
pluginImplPubspecContent = pluginImplPubspecContent.replaceFirst( pluginImplPubspecContent = pluginImplPubspecContent.replaceFirst(
' platforms:\n', ' platforms:\n',
' implements: plugin_platform_interface\n' ' implements: aplugin_platform_interface\n'
' platforms:\n'); ' platforms:\n');
await pluginImplPubspec.writeAsString(pluginImplPubspecContent, await pluginImplPubspec.writeAsString(pluginImplPubspecContent,
flush: true); flush: true);
...@@ -85,28 +85,28 @@ class PluginPlatformInterfaceMacOS { ...@@ -85,28 +85,28 @@ class PluginPlatformInterfaceMacOS {
'io.flutter.devicelab', 'io.flutter.devicelab',
'--platforms', '--platforms',
'macos', 'macos',
'plugin_platform_interface', 'aplugin_platform_interface',
], ],
environment: environment, environment: environment,
); );
}); });
final File pluginInterfacePubspec = File(path.join( final File pluginInterfacePubspec = File(path.join(
tempDir.absolute.path, tempDir.absolute.path,
'plugin_platform_interface', 'aplugin_platform_interface',
'pubspec.yaml', 'pubspec.yaml',
)); ));
String pluginInterfacePubspecContent = String pluginInterfacePubspecContent =
await pluginInterfacePubspec.readAsString(); await pluginInterfacePubspec.readAsString();
pluginInterfacePubspecContent = pluginInterfacePubspecContent =
pluginInterfacePubspecContent.replaceFirst( pluginInterfacePubspecContent.replaceFirst(
' pluginClass: PluginPlatformInterfacePlugin', ' pluginClass: ApluginPlatformInterfacePlugin',
' default_package: plugin_platform_implementation\n'); ' default_package: aplugin_platform_implementation\n');
pluginInterfacePubspecContent = pluginInterfacePubspecContent =
pluginInterfacePubspecContent.replaceFirst( pluginInterfacePubspecContent.replaceFirst(
'dependencies:', 'dependencies:',
'dependencies:\n' 'dependencies:\n'
' plugin_platform_implementation:\n' ' aplugin_platform_implementation:\n'
' path: ../plugin_platform_implementation\n'); ' path: ../aplugin_platform_implementation\n');
await pluginInterfacePubspec.writeAsString(pluginInterfacePubspecContent, await pluginInterfacePubspec.writeAsString(pluginInterfacePubspecContent,
flush: true); flush: true);
...@@ -136,8 +136,8 @@ class PluginPlatformInterfaceMacOS { ...@@ -136,8 +136,8 @@ class PluginPlatformInterfaceMacOS {
appPubspecContent = appPubspecContent.replaceFirst( appPubspecContent = appPubspecContent.replaceFirst(
'dependencies:', 'dependencies:',
'dependencies:\n' 'dependencies:\n'
' plugin_platform_interface:\n' ' aplugin_platform_interface:\n'
' path: ../plugin_platform_interface\n'); ' path: ../aplugin_platform_interface\n');
await appPubspec.writeAsString(appPubspecContent, flush: true); await appPubspec.writeAsString(appPubspecContent, flush: true);
section('Flutter run for macos'); section('Flutter run for macos');
...@@ -153,7 +153,7 @@ class PluginPlatformInterfaceMacOS { ...@@ -153,7 +153,7 @@ class PluginPlatformInterfaceMacOS {
.transform<String>(const LineSplitter()) .transform<String>(const LineSplitter())
.listen((String line) { .listen((String line) {
if (line.contains( if (line.contains(
'PluginPlatformInterfaceMacOS.registerWith() was called')) { 'ApluginPlatformInterfaceMacOS.registerWith() was called')) {
registryExecutedCompleter.complete(); registryExecutedCompleter.complete();
} }
print('stdout: $line'); print('stdout: $line');
......
...@@ -358,6 +358,8 @@ abstract class CreateBase extends FlutterCommand { ...@@ -358,6 +358,8 @@ abstract class CreateBase extends FlutterCommand {
final String pluginClassSnakeCase = snakeCase(pluginClass); final String pluginClassSnakeCase = snakeCase(pluginClass);
final String pluginClassCapitalSnakeCase = final String pluginClassCapitalSnakeCase =
pluginClassSnakeCase.toUpperCase(); pluginClassSnakeCase.toUpperCase();
final String pluginClassLowerCamelCase =
pluginClass[0].toLowerCase() + pluginClass.substring(1);
final String appleIdentifier = final String appleIdentifier =
createUTIIdentifier(organization, projectName); createUTIIdentifier(organization, projectName);
final String androidIdentifier = final String androidIdentifier =
...@@ -389,6 +391,7 @@ abstract class CreateBase extends FlutterCommand { ...@@ -389,6 +391,7 @@ abstract class CreateBase extends FlutterCommand {
'androidSdkVersion': kAndroidSdkMinVersion, 'androidSdkVersion': kAndroidSdkMinVersion,
'pluginClass': pluginClass, 'pluginClass': pluginClass,
'pluginClassSnakeCase': pluginClassSnakeCase, 'pluginClassSnakeCase': pluginClassSnakeCase,
'pluginClassLowerCamelCase': pluginClassLowerCamelCase,
'pluginClassCapitalSnakeCase': pluginClassCapitalSnakeCase, 'pluginClassCapitalSnakeCase': pluginClassCapitalSnakeCase,
'pluginDartClass': pluginDartClass, 'pluginDartClass': pluginDartClass,
'pluginProjectUUID': const Uuid().v4().toUpperCase(), 'pluginProjectUUID': const Uuid().v4().toUpperCase(),
......
...@@ -136,6 +136,7 @@ class MyApp extends StatefulWidget { ...@@ -136,6 +136,7 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> { class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown'; String _platformVersion = 'Unknown';
final _{{pluginClassLowerCamelCase}} = {{pluginDartClass}}();
@override @override
void initState() { void initState() {
...@@ -150,7 +151,7 @@ class _MyAppState extends State<MyApp> { ...@@ -150,7 +151,7 @@ class _MyAppState extends State<MyApp> {
// We also handle the message potentially returning null. // We also handle the message potentially returning null.
try { try {
platformVersion = platformVersion =
await {{pluginDartClass}}.platformVersion ?? 'Unknown platform version'; await _{{pluginClassLowerCamelCase}}.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException { } on PlatformException {
platformVersion = 'Failed to get platform version.'; platformVersion = 'Failed to get platform version.';
} }
......
{{#no_platforms}} {{#no_platforms}}
// You have generated a new plugin project without // You have generated a new plugin project without specifying the `--platforms`
// specifying the `--platforms` flag. A plugin project supports no platforms is generated. // flag. A plugin project with no platform support was generated. To add a
// To add platforms, run `flutter create -t plugin --platforms <platforms> .` under the same // platform, run `flutter create -t plugin --platforms <platforms> .` under the
// 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. // 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.
{{/no_platforms}} {{/no_platforms}}
import 'dart:async'; import '{{projectName}}_platform_interface.dart';
import 'package:flutter/services.dart';
class {{pluginDartClass}} { class {{pluginDartClass}} {
static const MethodChannel _channel = MethodChannel('{{projectName}}'); Future<String?> getPlatformVersion() {
return {{pluginDartClass}}Platform.instance.getPlatformVersion();
static Future<String?> get platformVersion async {
final String? version = await _channel.invokeMethod('getPlatformVersion');
return version;
} }
} }
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import '{{projectName}}_platform_interface.dart';
/// An implementation of [{{pluginDartClass}}Platform] that uses method channels.
class MethodChannel{{pluginDartClass}} extends {{pluginDartClass}}Platform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('{{projectName}}');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
}
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import '{{projectName}}_method_channel.dart';
abstract class {{pluginDartClass}}Platform extends PlatformInterface {
/// Constructs a {{pluginDartClass}}Platform.
{{pluginDartClass}}Platform() : super(token: _token);
static final Object _token = Object();
static {{pluginDartClass}}Platform _instance = MethodChannel{{pluginDartClass}}();
/// The default instance of [{{pluginDartClass}}Platform] to use.
///
/// Defaults to [MethodChannel{{pluginDartClass}}].
static {{pluginDartClass}}Platform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [{{pluginDartClass}}Platform] when
/// they register themselves.
static set instance({{pluginDartClass}}Platform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}
import 'dart:async';
// In order to *not* need this ignore, consider extracting the "web" version // In order to *not* need this ignore, consider extracting the "web" version
// of your plugin as a separate package, instead of inlining it in the same // of your plugin as a separate package, instead of inlining it in the same
// package as the core of your plugin. // package as the core of your plugin.
// ignore: avoid_web_libraries_in_flutter // ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html show window; import 'dart:html' as html show window;
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart';
/// A web implementation of the {{pluginDartClass}} plugin. import '{{projectName}}_platform_interface.dart';
class {{pluginDartClass}}Web {
static void registerWith(Registrar registrar) {
final MethodChannel channel = MethodChannel(
'{{projectName}}',
const StandardMethodCodec(),
registrar,
);
final pluginInstance = {{pluginDartClass}}Web(); /// A web implementation of the {{pluginDartClass}}Platform of the {{pluginDartClass}} plugin.
channel.setMethodCallHandler(pluginInstance.handleMethodCall); class {{pluginDartClass}}Web extends {{pluginDartClass}}Platform {
} /// Constructs a {{pluginDartClass}}Web
{{pluginDartClass}}Web();
/// Handles method calls over the MethodChannel of this plugin. static void registerWith(Registrar registrar) {
/// Note: Check the "federated" architecture for a new way of doing this: {{pluginDartClass}}Platform.instance = {{pluginDartClass}}Web();
/// https://flutter.dev/go/federated-plugins
Future<dynamic> handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'getPlatformVersion':
return getPlatformVersion();
default:
throw PlatformException(
code: 'Unimplemented',
details: '{{projectName}} for web doesn\'t implement \'${call.method}\'',
);
}
} }
/// Returns a [String] containing the version of the platform. /// Returns a [String] containing the version of the platform.
Future<String> getPlatformVersion() { @override
Future<String?> getPlatformVersion() async {
final version = html.window.navigator.userAgent; final version = html.window.navigator.userAgent;
return Future.value(version); return version;
} }
} }
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:{{projectName}}/{{projectName}}_method_channel.dart';
void main() {
MethodChannel{{pluginDartClass}} platform = MethodChannel{{pluginDartClass}}();
const MethodChannel channel = MethodChannel('{{projectName}}');
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
channel.setMockMethodCallHandler((MethodCall methodCall) async {
return '42';
});
});
tearDown(() {
channel.setMockMethodCallHandler(null);
});
test('getPlatformVersion', () async {
expect(await platform.getPlatformVersion(), '42');
});
}
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:{{projectName}}/{{projectName}}.dart'; import 'package:{{projectName}}/{{projectName}}.dart';
import 'package:{{projectName}}/{{projectName}}_platform_interface.dart';
import 'package:{{projectName}}/{{projectName}}_method_channel.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
void main() { class Mock{{pluginDartClass}}Platform
const MethodChannel channel = MethodChannel('{{projectName}}'); with MockPlatformInterfaceMixin
implements {{pluginDartClass}}Platform {
TestWidgetsFlutterBinding.ensureInitialized(); @override
Future<String?> getPlatformVersion() => Future.value('42');
}
setUp(() { void main() {
channel.setMockMethodCallHandler((MethodCall methodCall) async { final {{pluginDartClass}}Platform initialPlatform = {{pluginDartClass}}Platform.instance;
return '42';
});
});
tearDown(() { test('$MethodChannel{{pluginDartClass}} is the default instance', () {
channel.setMockMethodCallHandler(null); expect(initialPlatform, isInstanceOf<MethodChannel{{pluginDartClass}}>());
}); });
test('getPlatformVersion', () async { test('getPlatformVersion', () async {
expect(await {{pluginDartClass}}.platformVersion, '42'); {{pluginDartClass}} {{pluginClassLowerCamelCase}} = {{pluginDartClass}}();
Mock{{pluginDartClass}}Platform fakePlatform = Mock{{pluginDartClass}}Platform();
{{pluginDartClass}}Platform.instance = fakePlatform;
expect(await {{pluginClassLowerCamelCase}}.getPlatformVersion(), '42');
}); });
} }
...@@ -19,6 +19,7 @@ dependencies: ...@@ -19,6 +19,7 @@ dependencies:
flutter_web_plugins: flutter_web_plugins:
sdk: flutter sdk: flutter
{{/web}} {{/web}}
plugin_platform_interface: ^2.0.2
dev_dependencies: dev_dependencies:
{{#withFfiPluginHook}} {{#withFfiPluginHook}}
......
...@@ -326,12 +326,15 @@ ...@@ -326,12 +326,15 @@
"templates/plugin/ios.tmpl/.gitignore", "templates/plugin/ios.tmpl/.gitignore",
"templates/plugin/ios.tmpl/Assets/.gitkeep", "templates/plugin/ios.tmpl/Assets/.gitkeep",
"templates/plugin/lib/projectName.dart.tmpl", "templates/plugin/lib/projectName.dart.tmpl",
"templates/plugin/lib/projectName_platform_interface.dart.tmpl",
"templates/plugin/lib/projectName_method_channel.dart.tmpl",
"templates/plugin/linux.tmpl/CMakeLists.txt.tmpl", "templates/plugin/linux.tmpl/CMakeLists.txt.tmpl",
"templates/plugin/linux.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl", "templates/plugin/linux.tmpl/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl",
"templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl", "templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl",
"templates/plugin/macos.tmpl/Classes/pluginClass.swift.tmpl", "templates/plugin/macos.tmpl/Classes/pluginClass.swift.tmpl",
"templates/plugin/README.md.tmpl", "templates/plugin/README.md.tmpl",
"templates/plugin/test/projectName_test.dart.tmpl", "templates/plugin/test/projectName_test.dart.tmpl",
"templates/plugin/test/projectName_method_channel_test.dart.tmpl",
"templates/plugin/windows.tmpl/CMakeLists.txt.tmpl", "templates/plugin/windows.tmpl/CMakeLists.txt.tmpl",
"templates/plugin/windows.tmpl/include/projectName.tmpl/pluginClassSnakeCase_c_api.h.tmpl", "templates/plugin/windows.tmpl/include/projectName.tmpl/pluginClassSnakeCase_c_api.h.tmpl",
"templates/plugin/windows.tmpl/pluginClassSnakeCase.cpp.tmpl", "templates/plugin/windows.tmpl/pluginClassSnakeCase.cpp.tmpl",
......
...@@ -499,6 +499,8 @@ void main() { ...@@ -499,6 +499,8 @@ void main() {
<String>['--template=plugin', '-i', 'objc', '-a', 'java'], <String>['--template=plugin', '-i', 'objc', '-a', 'java'],
<String>[ <String>[
'analysis_options.yaml', 'analysis_options.yaml',
'LICENSE',
'README.md',
'example/lib/main.dart', 'example/lib/main.dart',
'flutter_project.iml', 'flutter_project.iml',
'lib/flutter_project.dart', 'lib/flutter_project.dart',
...@@ -2064,6 +2066,53 @@ void main() { ...@@ -2064,6 +2066,53 @@ void main() {
FeatureFlags: () => TestFeatureFlags(), FeatureFlags: () => TestFeatureFlags(),
}); });
testUsingContext('plugin creates platform interface by default', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]);
expect(projectDir.childDirectory('lib').childFile('flutter_project_method_channel.dart'),
exists);
expect(projectDir.childDirectory('lib').childFile('flutter_project_platform_interface.dart'),
exists);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(),
});
testUsingContext('plugin passes analysis and unit tests', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]);
await _getPackages(projectDir);
await _analyzeProject(projectDir.path);
await _runFlutterTest(projectDir);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(),
});
testUsingContext('plugin example passes analysis and unit tests', () async {
Cache.flutterRoot = '../..';
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]);
final Directory exampleDir = projectDir.childDirectory('example');
await _getPackages(exampleDir);
await _analyzeProject(exampleDir.path);
await _runFlutterTest(exampleDir);
});
testUsingContext('plugin supports ios if requested', () async { testUsingContext('plugin supports ios if requested', () async {
Cache.flutterRoot = '../..'; Cache.flutterRoot = '../..';
...@@ -2123,6 +2172,10 @@ void main() { ...@@ -2123,6 +2172,10 @@ void main() {
androidIdentifier: 'com.example.flutter_project', androidIdentifier: 'com.example.flutter_project',
webFileName: 'flutter_project_web.dart'); webFileName: 'flutter_project_web.dart');
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage))); expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
await _getPackages(projectDir);
await _analyzeProject(projectDir.path);
await _runFlutterTest(projectDir);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true), FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
Logger: () => logger, Logger: () => logger,
...@@ -3021,7 +3074,7 @@ Future<void> _analyzeProject(String workingDir, { List<String> expectedFailures ...@@ -3021,7 +3074,7 @@ Future<void> _analyzeProject(String workingDir, { List<String> expectedFailures
expect(errors, unorderedEquals(expectedFailures)); expect(errors, unorderedEquals(expectedFailures));
} }
Future<void> _runFlutterTest(Directory workingDir, { String target }) async { Future<void> _getPackages(Directory workingDir) async {
final String flutterToolsSnapshotPath = globals.fs.path.absolute(globals.fs.path.join( final String flutterToolsSnapshotPath = globals.fs.path.absolute(globals.fs.path.join(
'..', '..',
'..', '..',
...@@ -3041,6 +3094,18 @@ Future<void> _runFlutterTest(Directory workingDir, { String target }) async { ...@@ -3041,6 +3094,18 @@ Future<void> _runFlutterTest(Directory workingDir, { String target }) async {
], ],
workingDirectory: workingDir.path, workingDirectory: workingDir.path,
); );
}
Future<void> _runFlutterTest(Directory workingDir, { String target }) async {
final String flutterToolsSnapshotPath = globals.fs.path.absolute(globals.fs.path.join(
'..',
'..',
'bin',
'cache',
'flutter_tools.snapshot',
));
await _getPackages(workingDir);
final List<String> args = <String>[ final List<String> args = <String>[
flutterToolsSnapshotPath, flutterToolsSnapshotPath,
......
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