flutter_flutter/packages/flutter_tools/lib/src/platform_plugins.dart
Jamil Saadeh f26eddba00
Null aware elements clean-ups (#173074)
<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->
Converted more null checks to null aware elements.
The analyzer had many false negatives and the regex became pretty wild
to find many of them 😂

I've found more in the engine folder but I'll apply the changes in a
separate PR

Part of #172188 

*If you had to change anything in the [flutter/tests] repo, include a
link to the migration guide as per the [breaking change policy].*

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-08-13 15:36:14 +00:00

660 lines
20 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:yaml/yaml.dart';
import 'base/common.dart';
import 'base/file_system.dart';
/// Constant for 'pluginClass' key in plugin maps.
const kPluginClass = 'pluginClass';
/// Constant for 'dartPluginClass' key in plugin maps.
const kDartPluginClass = 'dartPluginClass';
/// Constant for 'dartPluginFile' key in plugin maps.
const kDartFileName = 'dartFileName';
/// Constant for 'ffiPlugin' key in plugin maps.
const kFfiPlugin = 'ffiPlugin';
// Constant for 'defaultPackage' key in plugin maps.
const kDefaultPackage = 'default_package';
/// Constant for 'sharedDarwinSource' key in plugin maps.
/// Can be set for iOS and macOS plugins.
const kSharedDarwinSource = 'sharedDarwinSource';
/// Constant for 'supportedVariants' key in plugin maps.
const kSupportedVariants = 'supportedVariants';
/// Platform variants that a Windows plugin can support.
enum PluginPlatformVariant {
/// Win32 variant of Windows.
win32,
}
/// Marker interface for all platform specific plugin config implementations.
abstract class PluginPlatform {
const PluginPlatform();
Map<String, dynamic> toMap();
}
/// A plugin that has platform variants.
abstract class VariantPlatformPlugin {
/// The platform variants supported by the plugin.
Set<PluginPlatformVariant> get supportedVariants;
}
abstract class NativeOrDartPlugin {
/// Determines whether the plugin has a Dart implementation.
bool hasDart();
/// Determines whether the plugin has a FFI implementation.
bool hasFfi();
/// Determines whether the plugin has a method channel implementation.
bool hasMethodChannel();
}
abstract class DarwinPlugin {
/// Indicates the iOS and macOS native code is shareable the subdirectory "darwin",
bool get sharedDarwinSource;
}
/// Contains parameters to template an Android plugin.
///
/// The [name] of the plugin is required. Additionally, either:
/// - [defaultPackage], or
/// - an implementation consisting of:
/// - the [package] and [pluginClass] that will be the entry point to the
/// plugin's native code, and/or
/// - the [dartPluginClass] with optional [dartFileName] that will be
/// the entry point for the plugin's Dart code
/// is required.
class AndroidPlugin extends PluginPlatform implements NativeOrDartPlugin {
AndroidPlugin({
required this.name,
required this.pluginPath,
this.package,
this.pluginClass,
this.dartPluginClass,
this.dartFileName,
bool? ffiPlugin,
this.defaultPackage,
required FileSystem fileSystem,
}) : _fileSystem = fileSystem,
ffiPlugin = ffiPlugin ?? false;
factory AndroidPlugin.fromYaml(
String name,
YamlMap yaml,
String pluginPath,
FileSystem fileSystem,
) {
assert(validate(yaml));
final dartPluginClass = yaml[kDartPluginClass] as String?;
final dartFileName = yaml[kDartFileName] as String?;
if (dartPluginClass == null && dartFileName != null) {
throwToolExit(
'"dartFileName" cannot be specified without "dartPluginClass" in Android platform of plugin "$name"',
);
}
return AndroidPlugin(
name: name,
package: yaml['package'] as String?,
pluginClass: yaml[kPluginClass] as String?,
dartPluginClass: dartPluginClass,
dartFileName: dartFileName,
ffiPlugin: yaml[kFfiPlugin] as bool? ?? false,
defaultPackage: yaml[kDefaultPackage] as String?,
pluginPath: pluginPath,
fileSystem: fileSystem,
);
}
final FileSystem _fileSystem;
@override
bool hasMethodChannel() => pluginClass != null;
@override
bool hasFfi() => ffiPlugin;
@override
bool hasDart() => dartPluginClass != null;
static bool validate(YamlMap yaml) {
return (yaml['package'] is String && yaml[kPluginClass] is String) ||
yaml[kDartPluginClass] is String ||
yaml[kFfiPlugin] == true ||
yaml[kDefaultPackage] is String;
}
static const kConfigKey = 'android';
/// The plugin name defined in pubspec.yaml.
final String name;
/// The plugin package name defined in pubspec.yaml.
final String? package;
/// The native plugin main class defined in pubspec.yaml, if any.
final String? pluginClass;
/// The Dart plugin main class defined in pubspec.yaml, if any.
final String? dartPluginClass;
/// Path to file in which dartPluginClass defined, if any.
final String? dartFileName;
/// Is FFI plugin defined in pubspec.yaml.
final bool ffiPlugin;
/// The default implementation package defined in pubspec.yaml, if any.
final String? defaultPackage;
/// The absolute path to the plugin in the pub cache.
final String pluginPath;
@override
Map<String, dynamic> toMap() {
return <String, dynamic>{
'name': name,
'package': ?package,
'class': ?pluginClass,
kDartPluginClass: ?dartPluginClass,
kDartFileName: ?dartFileName,
if (ffiPlugin) kFfiPlugin: true,
kDefaultPackage: ?defaultPackage,
// Mustache doesn't support complex types.
'supportsEmbeddingV1': _supportedEmbeddings.contains('1'),
'supportsEmbeddingV2': _supportedEmbeddings.contains('2'),
};
}
/// Returns the version of the Android embedding.
late final Set<String> _supportedEmbeddings = _getSupportedEmbeddings();
Set<String> _getSupportedEmbeddings() {
final supportedEmbeddings = <String>{};
final String baseMainPath = _fileSystem.path.join(pluginPath, 'android', 'src', 'main');
final String? package = this.package;
// Don't attempt to validate the native code if there isn't supposed to
// be any.
if (package == null) {
return supportedEmbeddings;
}
final mainClassCandidates = <String>[
_fileSystem.path.join(
baseMainPath,
'java',
package.replaceAll('.', _fileSystem.path.separator),
'$pluginClass.java',
),
_fileSystem.path.join(
baseMainPath,
'kotlin',
package.replaceAll('.', _fileSystem.path.separator),
'$pluginClass.kt',
),
];
File? mainPluginClass;
var mainClassFound = false;
for (final mainClassCandidate in mainClassCandidates) {
mainPluginClass = _fileSystem.file(mainClassCandidate);
if (mainPluginClass.existsSync()) {
mainClassFound = true;
break;
}
}
if (mainPluginClass == null || !mainClassFound) {
assert(mainClassCandidates.length <= 2);
throwToolExit(
"The plugin `$name` doesn't have a main class defined in ${mainClassCandidates.join(' or ')}. "
"This is likely to due to an incorrect `androidPackage: $package` or `mainClass` entry in the plugin's pubspec.yaml.\n"
'If you are the author of this plugin, fix the `androidPackage` entry or move the main class to any of locations used above. '
'Otherwise, please contact the author of this plugin and consider using a different plugin in the meanwhile. ',
);
}
final String mainClassContent = mainPluginClass.readAsStringSync();
if (mainClassContent.contains('io.flutter.embedding.engine.plugins.FlutterPlugin')) {
supportedEmbeddings.add('2');
} else {
supportedEmbeddings.add('1');
}
if (mainClassContent.contains('PluginRegistry') && mainClassContent.contains('registerWith')) {
supportedEmbeddings.add('1');
}
return supportedEmbeddings;
}
}
/// Contains the parameters to template an iOS plugin.
///
/// The [name] of the plugin is required. Additionally, either:
/// - [defaultPackage], or
/// - an implementation consisting of:
/// - the [classPrefix] (with optional [pluginClass]) that will be the entry
/// point to the plugin's native code, and/or
/// - the [dartPluginClass] with optional [dartFileName] that will be
/// the entry point for the plugin's Dart code
/// is required.
class IOSPlugin extends PluginPlatform implements NativeOrDartPlugin, DarwinPlugin {
const IOSPlugin({
required this.name,
required this.classPrefix,
this.pluginClass,
this.dartPluginClass,
this.dartFileName,
bool? ffiPlugin,
this.defaultPackage,
bool? sharedDarwinSource,
}) : ffiPlugin = ffiPlugin ?? false,
sharedDarwinSource = sharedDarwinSource ?? false;
factory IOSPlugin.fromYaml(String name, YamlMap yaml) {
assert(validate(yaml)); // TODO(zanderso): https://github.com/flutter/flutter/issues/67241
final dartPluginClass = yaml[kDartPluginClass] as String?;
final dartFileName = yaml[kDartFileName] as String?;
if (dartPluginClass == null && dartFileName != null) {
throwToolExit(
'"dartFileName" cannot be specified without "dartPluginClass" in iOS platform of plugin "$name"',
);
}
return IOSPlugin(
name: name,
classPrefix: '',
pluginClass: yaml[kPluginClass] as String?,
dartPluginClass: dartPluginClass,
dartFileName: dartFileName,
ffiPlugin: yaml[kFfiPlugin] as bool? ?? false,
defaultPackage: yaml[kDefaultPackage] as String?,
sharedDarwinSource: yaml[kSharedDarwinSource] as bool? ?? false,
);
}
static bool validate(YamlMap yaml) {
return yaml[kPluginClass] is String ||
yaml[kDartPluginClass] is String ||
yaml[kFfiPlugin] == true ||
yaml[kSharedDarwinSource] == true ||
yaml[kDefaultPackage] is String;
}
static const kConfigKey = 'ios';
final String name;
/// Note, this is here only for legacy reasons. Multi-platform format
/// always sets it to empty String.
final String classPrefix;
final String? pluginClass;
final String? dartPluginClass;
final String? dartFileName;
final bool ffiPlugin;
final String? defaultPackage;
/// Indicates the iOS native code is shareable with macOS in
/// the subdirectory "darwin", otherwise in the subdirectory "ios".
@override
final bool sharedDarwinSource;
@override
bool hasMethodChannel() => pluginClass != null;
@override
bool hasFfi() => ffiPlugin;
@override
bool hasDart() => dartPluginClass != null;
@override
Map<String, dynamic> toMap() {
return <String, dynamic>{
'name': name,
'prefix': classPrefix,
'class': ?pluginClass,
kDartPluginClass: ?dartPluginClass,
kDartFileName: ?dartFileName,
if (ffiPlugin) kFfiPlugin: true,
if (sharedDarwinSource) kSharedDarwinSource: true,
kDefaultPackage: ?defaultPackage,
};
}
}
/// Contains the parameters to template a macOS plugin.
///
/// The [name] of the plugin is required. Either [dartPluginClass] or
/// [pluginClass] or [ffiPlugin] are required.
/// [pluginClass] will be the entry point to the plugin's native code.
/// [dartFileName] is not required and will be used only if [dartPluginClass]
/// provided.
class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin, DarwinPlugin {
const MacOSPlugin({
required this.name,
this.pluginClass,
this.dartPluginClass,
this.dartFileName,
bool? ffiPlugin,
this.defaultPackage,
bool? sharedDarwinSource,
}) : ffiPlugin = ffiPlugin ?? false,
sharedDarwinSource = sharedDarwinSource ?? false;
factory MacOSPlugin.fromYaml(String name, YamlMap yaml) {
assert(validate(yaml));
final dartPluginClass = yaml[kDartPluginClass] as String?;
final dartFileName = yaml[kDartFileName] as String?;
if (dartPluginClass == null && dartFileName != null) {
throwToolExit(
'"dartFileName" cannot be specified without "dartPluginClass" in macOS platform of plugin "$name"',
);
}
return MacOSPlugin(
name: name,
pluginClass: yaml[kPluginClass] as String?,
dartPluginClass: dartPluginClass,
dartFileName: dartFileName,
ffiPlugin: yaml[kFfiPlugin] as bool?,
defaultPackage: yaml[kDefaultPackage] as String?,
sharedDarwinSource: yaml[kSharedDarwinSource] as bool?,
);
}
static bool validate(YamlMap yaml) {
return yaml[kPluginClass] is String ||
yaml[kDartPluginClass] is String ||
yaml[kFfiPlugin] == true ||
yaml[kSharedDarwinSource] == true ||
yaml[kDefaultPackage] is String;
}
static const kConfigKey = 'macos';
final String name;
final String? pluginClass;
final String? dartPluginClass;
final String? dartFileName;
final bool ffiPlugin;
final String? defaultPackage;
/// Indicates the macOS native code is shareable with iOS in
/// the subdirectory "darwin", otherwise in the subdirectory "macos".
@override
final bool sharedDarwinSource;
@override
bool hasMethodChannel() => pluginClass != null;
@override
bool hasFfi() => ffiPlugin;
@override
bool hasDart() => dartPluginClass != null;
@override
Map<String, dynamic> toMap() {
return <String, dynamic>{
'name': name,
'class': ?pluginClass,
kDartPluginClass: ?dartPluginClass,
kDartFileName: ?dartFileName,
if (ffiPlugin) kFfiPlugin: true,
if (sharedDarwinSource) kSharedDarwinSource: true,
kDefaultPackage: ?defaultPackage,
};
}
}
/// Contains the parameters to template a Windows plugin.
///
/// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required.
/// [pluginClass] will be the entry point to the plugin's native code.
/// [dartFileName] is not required and will be used only if [dartPluginClass]
/// provided.
class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, VariantPlatformPlugin {
const WindowsPlugin({
required this.name,
this.pluginClass,
this.dartPluginClass,
this.dartFileName,
bool? ffiPlugin,
this.defaultPackage,
this.variants = const <PluginPlatformVariant>{},
}) : ffiPlugin = ffiPlugin ?? false,
assert(pluginClass != null || dartPluginClass != null || defaultPackage != null);
factory WindowsPlugin.fromYaml(String name, YamlMap yaml) {
assert(validate(yaml));
final pluginClass = yaml[kPluginClass] as String?;
final variants = <PluginPlatformVariant>{};
final variantList = yaml[kSupportedVariants] as YamlList?;
if (variantList == null) {
// If no variant list is provided assume Win32 for backward compatibility.
variants.add(PluginPlatformVariant.win32);
} else {
const variantByName = <String, PluginPlatformVariant>{'win32': PluginPlatformVariant.win32};
for (final String variantName in variantList.cast<String>()) {
final PluginPlatformVariant? variant = variantByName[variantName];
if (variant != null) {
variants.add(variant);
}
// Ignore unrecognized variants to make adding new variants in the
// future non-breaking.
}
}
final dartPluginClass = yaml[kDartPluginClass] as String?;
final dartFileName = yaml[kDartFileName] as String?;
if (dartPluginClass == null && dartFileName != null) {
throwToolExit(
'"dartFileName" cannot be specified without "dartPluginClass" in Windows platform of plugin "$name"',
);
}
return WindowsPlugin(
name: name,
pluginClass: pluginClass,
dartPluginClass: dartPluginClass,
dartFileName: dartFileName,
ffiPlugin: yaml[kFfiPlugin] as bool?,
defaultPackage: yaml[kDefaultPackage] as String?,
variants: variants,
);
}
static bool validate(YamlMap yaml) {
return yaml[kPluginClass] is String ||
yaml[kDartPluginClass] is String ||
yaml[kFfiPlugin] == true ||
yaml[kDefaultPackage] is String;
}
static const kConfigKey = 'windows';
final String name;
final String? pluginClass;
final String? dartPluginClass;
final String? dartFileName;
final bool ffiPlugin;
final String? defaultPackage;
final Set<PluginPlatformVariant> variants;
@override
Set<PluginPlatformVariant> get supportedVariants => variants;
@override
bool hasMethodChannel() => pluginClass != null;
@override
bool hasFfi() => ffiPlugin;
@override
bool hasDart() => dartPluginClass != null;
@override
Map<String, dynamic> toMap() {
return <String, dynamic>{
'name': name,
'class': ?pluginClass,
if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!),
kDartPluginClass: ?dartPluginClass,
kDartFileName: ?dartFileName,
if (ffiPlugin) kFfiPlugin: true,
kDefaultPackage: ?defaultPackage,
};
}
}
/// Contains the parameters to template a Linux plugin.
///
/// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required.
/// [pluginClass] will be the entry point to the plugin's native code.
/// [dartFileName] is not required and will be used only if [dartPluginClass]
/// provided.
class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin {
const LinuxPlugin({
required this.name,
this.pluginClass,
this.dartPluginClass,
this.dartFileName,
bool? ffiPlugin,
this.defaultPackage,
}) : ffiPlugin = ffiPlugin ?? false,
assert(
pluginClass != null ||
dartPluginClass != null ||
(ffiPlugin ?? false) ||
defaultPackage != null,
);
factory LinuxPlugin.fromYaml(String name, YamlMap yaml) {
assert(validate(yaml));
final dartPluginClass = yaml[kDartPluginClass] as String?;
final dartFileName = yaml[kDartFileName] as String?;
if (dartPluginClass == null && dartFileName != null) {
throwToolExit(
'"dartFileName" cannot be specified without "dartPluginClass" in Linux platform of plugin "$name"',
);
}
return LinuxPlugin(
name: name,
pluginClass: yaml[kPluginClass] as String?,
dartPluginClass: dartPluginClass,
dartFileName: dartFileName,
ffiPlugin: yaml[kFfiPlugin] as bool? ?? false,
defaultPackage: yaml[kDefaultPackage] as String?,
);
}
static bool validate(YamlMap yaml) {
return yaml[kPluginClass] is String ||
yaml[kDartPluginClass] is String ||
yaml[kFfiPlugin] == true ||
yaml[kDefaultPackage] is String;
}
static const kConfigKey = 'linux';
final String name;
final String? pluginClass;
final String? dartPluginClass;
final String? dartFileName;
final bool ffiPlugin;
final String? defaultPackage;
@override
bool hasMethodChannel() => pluginClass != null;
@override
bool hasFfi() => ffiPlugin;
@override
bool hasDart() => dartPluginClass != null;
@override
Map<String, dynamic> toMap() {
return <String, dynamic>{
'name': name,
'class': ?pluginClass,
if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!),
kDartPluginClass: ?dartPluginClass,
kDartFileName: ?dartFileName,
if (ffiPlugin) kFfiPlugin: true,
kDefaultPackage: ?defaultPackage,
};
}
}
/// Contains the parameters to template a web plugin.
///
/// The required fields include: [name] of the plugin, the [pluginClass] that will
/// be the entry point to the plugin's implementation, and the [fileName]
/// containing the code.
class WebPlugin extends PluginPlatform {
const WebPlugin({required this.name, required this.pluginClass, required this.fileName});
factory WebPlugin.fromYaml(String name, YamlMap yaml) {
if (yaml['pluginClass'] is! String) {
throwToolExit(
'The plugin `$name` is missing the required field `pluginClass` in pubspec.yaml',
);
}
if (yaml['fileName'] is! String) {
throwToolExit('The plugin `$name` is missing the required field `fileName` in pubspec.yaml');
}
return WebPlugin(
name: name,
pluginClass: yaml['pluginClass'] as String,
fileName: yaml['fileName'] as String,
);
}
static const kConfigKey = 'web';
/// The name of the plugin.
final String name;
/// The class containing the plugin implementation details.
///
/// This class should have a static `registerWith` method defined.
final String pluginClass;
/// The name of the file containing the class implementation above.
final String fileName;
@override
Map<String, dynamic> toMap() {
return <String, dynamic>{'name': name, 'class': pluginClass, 'file': fileName};
}
}
final _internalCapitalLetterRegex = RegExp(r'(?=(?!^)[A-Z])');
String _filenameForCppClass(String className) {
return className.splitMapJoin(
_internalCapitalLetterRegex,
onMatch: (_) => '_',
onNonMatch: (String n) => n.toLowerCase(),
);
}