First pass at using platform abstraction for plugins (#92672)
This commit is contained in:
parent
e8af40f06b
commit
0aab22807c
@ -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.';
|
||||
}
|
||||
|
@ -1,19 +1,16 @@
|
||||
{{#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();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
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.');
|
||||
}
|
||||
}
|
@ -1,44 +1,26 @@
|
||||
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 {
|
||||
import '{{projectName}}_platform_interface.dart';
|
||||
|
||||
/// A web implementation of the {{pluginDartClass}}Platform of the {{pluginDartClass}} plugin.
|
||||
class {{pluginDartClass}}Web extends {{pluginDartClass}}Platform {
|
||||
/// Constructs a {{pluginDartClass}}Web
|
||||
{{pluginDartClass}}Web();
|
||||
|
||||
static void registerWith(Registrar registrar) {
|
||||
final MethodChannel channel = MethodChannel(
|
||||
'{{projectName}}',
|
||||
const StandardMethodCodec(),
|
||||
registrar,
|
||||
);
|
||||
|
||||
final pluginInstance = {{pluginDartClass}}Web();
|
||||
channel.setMethodCallHandler(pluginInstance.handleMethodCall);
|
||||
}
|
||||
|
||||
/// 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}\'',
|
||||
);
|
||||
}
|
||||
{{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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
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');
|
||||
});
|
||||
}
|
@ -1,23 +1,29 @@
|
||||
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';
|
||||
|
||||
class Mock{{pluginDartClass}}Platform
|
||||
with MockPlatformInterfaceMixin
|
||||
implements {{pluginDartClass}}Platform {
|
||||
|
||||
@override
|
||||
Future<String?> getPlatformVersion() => Future.value('42');
|
||||
}
|
||||
|
||||
void main() {
|
||||
const MethodChannel channel = MethodChannel('{{projectName}}');
|
||||
final {{pluginDartClass}}Platform initialPlatform = {{pluginDartClass}}Platform.instance;
|
||||
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
channel.setMockMethodCallHandler((MethodCall methodCall) async {
|
||||
return '42';
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user