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({
'io.flutter.devicelab',
'--platforms',
'macos',
'plugin_platform_implementation',
'aplugin_platform_implementation',
],
environment: environment,
);
......@@ -39,9 +39,9 @@ TaskFunction dartPluginRegistryTest({
final File pluginMain = File(path.join(
tempDir.absolute.path,
'plugin_platform_implementation',
'aplugin_platform_implementation',
'lib',
'plugin_platform_implementation.dart',
'aplugin_platform_implementation.dart',
));
if (!pluginMain.existsSync()) {
return TaskResult.failure('${pluginMain.path} does not exist');
......@@ -49,9 +49,9 @@ TaskFunction dartPluginRegistryTest({
// Patch plugin main dart file.
await pluginMain.writeAsString('''
class PluginPlatformInterfaceMacOS {
class ApluginPlatformInterfaceMacOS {
static void registerWith() {
print('PluginPlatformInterfaceMacOS.registerWith() was called');
print('ApluginPlatformInterfaceMacOS.registerWith() was called');
}
}
''', flush: true);
......@@ -59,18 +59,18 @@ class PluginPlatformInterfaceMacOS {
// Patch plugin main pubspec file.
final File pluginImplPubspec = File(path.join(
tempDir.absolute.path,
'plugin_platform_implementation',
'aplugin_platform_implementation',
'pubspec.yaml',
));
String pluginImplPubspecContent = await pluginImplPubspec.readAsString();
pluginImplPubspecContent = pluginImplPubspecContent.replaceFirst(
' pluginClass: PluginPlatformImplementationPlugin',
' pluginClass: PluginPlatformImplementationPlugin\n'
' dartPluginClass: PluginPlatformInterfaceMacOS\n',
' pluginClass: ApluginPlatformImplementationPlugin',
' pluginClass: ApluginPlatformImplementationPlugin\n'
' dartPluginClass: ApluginPlatformInterfaceMacOS\n',
);
pluginImplPubspecContent = pluginImplPubspecContent.replaceFirst(
' platforms:\n',
' implements: plugin_platform_interface\n'
' implements: aplugin_platform_interface\n'
' platforms:\n');
await pluginImplPubspec.writeAsString(pluginImplPubspecContent,
flush: true);
......@@ -85,28 +85,28 @@ class PluginPlatformInterfaceMacOS {
'io.flutter.devicelab',
'--platforms',
'macos',
'plugin_platform_interface',
'aplugin_platform_interface',
],
environment: environment,
);
});
final File pluginInterfacePubspec = File(path.join(
tempDir.absolute.path,
'plugin_platform_interface',
'aplugin_platform_interface',
'pubspec.yaml',
));
String pluginInterfacePubspecContent =
await pluginInterfacePubspec.readAsString();
pluginInterfacePubspecContent =
pluginInterfacePubspecContent.replaceFirst(
' pluginClass: PluginPlatformInterfacePlugin',
' default_package: plugin_platform_implementation\n');
' pluginClass: ApluginPlatformInterfacePlugin',
' default_package: aplugin_platform_implementation\n');
pluginInterfacePubspecContent =
pluginInterfacePubspecContent.replaceFirst(
'dependencies:',
'dependencies:\n'
' plugin_platform_implementation:\n'
' path: ../plugin_platform_implementation\n');
' aplugin_platform_implementation:\n'
' path: ../aplugin_platform_implementation\n');
await pluginInterfacePubspec.writeAsString(pluginInterfacePubspecContent,
flush: true);
......@@ -136,8 +136,8 @@ class PluginPlatformInterfaceMacOS {
appPubspecContent = appPubspecContent.replaceFirst(
'dependencies:',
'dependencies:\n'
' plugin_platform_interface:\n'
' path: ../plugin_platform_interface\n');
' aplugin_platform_interface:\n'
' path: ../aplugin_platform_interface\n');
await appPubspec.writeAsString(appPubspecContent, flush: true);
section('Flutter run for macos');
......@@ -153,7 +153,7 @@ class PluginPlatformInterfaceMacOS {
.transform<String>(const LineSplitter())
.listen((String line) {
if (line.contains(
'PluginPlatformInterfaceMacOS.registerWith() was called')) {
'ApluginPlatformInterfaceMacOS.registerWith() was called')) {
registryExecutedCompleter.complete();
}
print('stdout: $line');
......
......@@ -358,6 +358,8 @@ abstract class CreateBase extends FlutterCommand {
final String pluginClassSnakeCase = snakeCase(pluginClass);
final String pluginClassCapitalSnakeCase =
pluginClassSnakeCase.toUpperCase();
final String pluginClassLowerCamelCase =
pluginClass[0].toLowerCase() + pluginClass.substring(1);
final String appleIdentifier =
createUTIIdentifier(organization, projectName);
final String androidIdentifier =
......@@ -389,6 +391,7 @@ abstract class CreateBase extends FlutterCommand {
'androidSdkVersion': kAndroidSdkMinVersion,
'pluginClass': pluginClass,
'pluginClassSnakeCase': pluginClassSnakeCase,
'pluginClassLowerCamelCase': pluginClassLowerCamelCase,
'pluginClassCapitalSnakeCase': pluginClassCapitalSnakeCase,
'pluginDartClass': pluginDartClass,
'pluginProjectUUID': const Uuid().v4().toUpperCase(),
......
......@@ -136,6 +136,7 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
final _{{pluginClassLowerCamelCase}} = {{pluginDartClass}}();
@override
void initState() {
......@@ -150,7 +151,7 @@ class _MyAppState extends State<MyApp> {
// We also handle the message potentially returning null.
try {
platformVersion =
await {{pluginDartClass}}.platformVersion ?? 'Unknown platform version';
await _{{pluginClassLowerCamelCase}}.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
......
{{#no_platforms}}
// You have generated a new plugin project without
// specifying the `--platforms` flag. A plugin project supports no platforms is generated.
// 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.
// You have generated a new plugin project without specifying the `--platforms`
// flag. A plugin project with no platform support was generated. To add a
// platform, 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.
{{/no_platforms}}
import 'dart:async';
import 'package:flutter/services.dart';
import '{{projectName}}_platform_interface.dart';
class {{pluginDartClass}} {
static const MethodChannel _channel = MethodChannel('{{projectName}}');
static Future<String?> get platformVersion async {
final String? version = await _channel.invokeMethod('getPlatformVersion');
return version;
Future<String?> getPlatformVersion() {
return {{pluginDartClass}}Platform.instance.getPlatformVersion();
}
}
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
// of your plugin as a separate package, instead of inlining it in the same
// package as the core of your plugin.
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html show window;
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
/// A web implementation of the {{pluginDartClass}} plugin.
class {{pluginDartClass}}Web {
static void registerWith(Registrar registrar) {
final MethodChannel channel = MethodChannel(
'{{projectName}}',
const StandardMethodCodec(),
registrar,
);
import '{{projectName}}_platform_interface.dart';
final pluginInstance = {{pluginDartClass}}Web();
channel.setMethodCallHandler(pluginInstance.handleMethodCall);
}
/// A web implementation of the {{pluginDartClass}}Platform of the {{pluginDartClass}} plugin.
class {{pluginDartClass}}Web extends {{pluginDartClass}}Platform {
/// Constructs a {{pluginDartClass}}Web
{{pluginDartClass}}Web();
/// Handles method calls over the MethodChannel of this plugin.
/// Note: Check the "federated" architecture for a new way of doing this:
/// 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}\'',
);
}
static void registerWith(Registrar registrar) {
{{pluginDartClass}}Platform.instance = {{pluginDartClass}}Web();
}
/// Returns a [String] containing the version of the platform.
Future<String> getPlatformVersion() {
@override
Future<String?> getPlatformVersion() async {
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:{{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() {
const MethodChannel channel = MethodChannel('{{projectName}}');
class Mock{{pluginDartClass}}Platform
with MockPlatformInterfaceMixin
implements {{pluginDartClass}}Platform {
TestWidgetsFlutterBinding.ensureInitialized();
@override
Future<String?> getPlatformVersion() => Future.value('42');
}
setUp(() {
channel.setMockMethodCallHandler((MethodCall methodCall) async {
return '42';
});
});
void main() {
final {{pluginDartClass}}Platform initialPlatform = {{pluginDartClass}}Platform.instance;
tearDown(() {
channel.setMockMethodCallHandler(null);
test('$MethodChannel{{pluginDartClass}} is the default instance', () {
expect(initialPlatform, isInstanceOf<MethodChannel{{pluginDartClass}}>());
});
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:
flutter_web_plugins:
sdk: flutter
{{/web}}
plugin_platform_interface: ^2.0.2
dev_dependencies:
{{#withFfiPluginHook}}
......
......@@ -326,12 +326,15 @@
"templates/plugin/ios.tmpl/.gitignore",
"templates/plugin/ios.tmpl/Assets/.gitkeep",
"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/include/projectName.tmpl/pluginClassSnakeCase.h.tmpl",
"templates/plugin/linux.tmpl/pluginClassSnakeCase.cc.tmpl",
"templates/plugin/macos.tmpl/Classes/pluginClass.swift.tmpl",
"templates/plugin/README.md.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/include/projectName.tmpl/pluginClassSnakeCase_c_api.h.tmpl",
"templates/plugin/windows.tmpl/pluginClassSnakeCase.cpp.tmpl",
......
......@@ -499,6 +499,8 @@ void main() {
<String>['--template=plugin', '-i', 'objc', '-a', 'java'],
<String>[
'analysis_options.yaml',
'LICENSE',
'README.md',
'example/lib/main.dart',
'flutter_project.iml',
'lib/flutter_project.dart',
......@@ -2064,6 +2066,53 @@ void main() {
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 {
Cache.flutterRoot = '../..';
......@@ -2123,6 +2172,10 @@ void main() {
androidIdentifier: 'com.example.flutter_project',
webFileName: 'flutter_project_web.dart');
expect(logger.errorText, isNot(contains(_kNoPlatformsMessage)));
await _getPackages(projectDir);
await _analyzeProject(projectDir.path);
await _runFlutterTest(projectDir);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isWebEnabled: true),
Logger: () => logger,
......@@ -3021,7 +3074,7 @@ Future<void> _analyzeProject(String workingDir, { List<String> 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(
'..',
'..',
......@@ -3041,6 +3094,18 @@ Future<void> _runFlutterTest(Directory workingDir, { String target }) async {
],
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>[
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