Commit 10f64830 authored by Mikkel Nygaard Ravn's avatar Mikkel Nygaard Ravn Committed by GitHub

Add Swift and Kotlin templates (#10259)

parent 230f1081
......@@ -46,6 +46,18 @@ class CreateCommand extends FlutterCommand {
defaultsTo: 'A new flutter project.',
help: 'The description to use for your new flutter project. This string ends up in the pubspec.yaml file.'
);
argParser.addOption(
'ios-language',
abbr: 'i',
defaultsTo: 'objc',
allowed: <String>['objc', 'swift'],
);
argParser.addOption(
'android-language',
abbr: 'a',
defaultsTo: 'java',
allowed: <String>['java', 'kotlin'],
);
}
@override
......@@ -109,9 +121,14 @@ class CreateCommand extends FlutterCommand {
throwToolExit(error);
final Map<String, dynamic> templateContext = _templateContext(
projectName, argResults['description'], dirPath,
flutterRoot, renderDriverTest: argResults['with-driver-test'],
withPluginHook: generatePlugin,
projectName: projectName,
projectDescription: argResults['description'],
dirPath: dirPath,
flutterRoot: flutterRoot,
renderDriverTest: argResults['with-driver-test'],
withPluginHook: generatePlugin,
androidLanguage: argResults['android-language'],
iosLanguage: argResults['ios-language'],
);
printStatus('Creating project ${fs.path.relative(dirPath)}...');
......@@ -206,9 +223,16 @@ To edit platform code in an IDE see https://flutter.io/platform-plugins/#edit-co
}
}
Map<String, dynamic> _templateContext(String projectName,
String projectDescription, String dirPath, String flutterRoot,
{ bool renderDriverTest: false, bool withPluginHook: false }) {
Map<String, dynamic> _templateContext({
String projectName,
String projectDescription,
String androidLanguage,
String iosLanguage,
String dirPath,
String flutterRoot,
bool renderDriverTest: false,
bool withPluginHook: false,
}) {
flutterRoot = fs.path.normalize(flutterRoot);
final String pluginDartClass = _createPluginClassName(projectName);
......@@ -229,6 +253,8 @@ To edit platform code in an IDE see https://flutter.io/platform-plugins/#edit-co
'pluginClass': pluginClass,
'pluginDartClass': pluginDartClass,
'withPluginHook': withPluginHook,
'androidLanguage': androidLanguage,
'iosLanguage': iosLanguage,
};
}
......
......@@ -126,10 +126,6 @@ const String _iosPluginRegistryHeaderTemplate = '''//
#import <Flutter/Flutter.h>
{{#plugins}}
#import "{{class}}.h"
{{/plugins}}
@interface GeneratedPluginRegistrant : NSObject
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
@end
......@@ -142,6 +138,9 @@ const String _iosPluginRegistryImplementationTemplate = '''//
//
#import "GeneratedPluginRegistrant.h"
{{#plugins}}
#import <{{name}}/{{class}}.h>
{{/plugins}}
@implementation GeneratedPluginRegistrant
......
......@@ -10,6 +10,7 @@ import 'globals.dart';
const String _kTemplateExtension = '.tmpl';
const String _kCopyTemplateExtension = '.copy.tmpl';
final Pattern _kTemplateLanguageVariant = new RegExp(r"(\w+)-(\w+)\.tmpl.*");
/// Expands templates in a directory to a destination. All files that must
/// undergo template expansion should end with the '.tmpl' extension. All other
......@@ -20,8 +21,11 @@ const String _kCopyTemplateExtension = '.copy.tmpl';
/// without template expansion (images, data files, etc.), the '.copy.tmpl'
/// extension may be used.
///
/// Files in the destination will not contain either the '.tmpl' or '.copy.tmpl'
/// extensions.
/// Folders with platform/language-specific content must be named
/// '<platform>-<language>.tmpl'.
///
/// Files in the destination will contain none of the '.tmpl', '.copy.tmpl'
/// or '-<language>.tmpl' extensions.
class Template {
Template(Directory templateSource, Directory baseDir) {
_templateFilePaths = <String, String>{};
......@@ -66,19 +70,38 @@ class Template {
destination.createSync(recursive: true);
int fileCount = 0;
final String projectName = context['projectName'];
final String pluginClass = context['pluginClass'];
final String destinationDirPath = destination.absolute.path;
_templateFilePaths.forEach((String relativeDestPath, String absoluteSrcPath) {
/// Returns the resolved destination path corresponding to the specified
/// raw destination path, after performing language filtering and template
/// expansion on the path itself.
///
/// Returns null if the given raw destination path has been filtered.
String renderPath(String relativeDestinationPath) {
final Match match = _kTemplateLanguageVariant.matchAsPrefix(relativeDestinationPath);
if (match != null) {
final String platform = match.group(1);
final String language = context['${platform}Language'];
if (language != match.group(2))
return null;
relativeDestinationPath = relativeDestinationPath.replaceAll('$platform-$language.tmpl', platform);
}
final String projectName = context['projectName'];
final String pluginClass = context['pluginClass'];
final String destinationDirPath = destination.absolute.path;
String finalDestinationPath = fs.path
.join(destinationDirPath, relativeDestPath)
.replaceAll(_kCopyTemplateExtension, '')
.replaceAll(_kTemplateExtension, '');
.join(destinationDirPath, relativeDestinationPath)
.replaceAll(_kCopyTemplateExtension, '')
.replaceAll(_kTemplateExtension, '');
if (projectName != null)
finalDestinationPath = finalDestinationPath.replaceAll('projectName', projectName);
if (pluginClass != null)
finalDestinationPath = finalDestinationPath.replaceAll('pluginClass', pluginClass);
return finalDestinationPath;
}
_templateFilePaths.forEach((String relativeDestinationPath, String absoluteSourcePath) {
final String finalDestinationPath = renderPath(relativeDestinationPath);
if (finalDestinationPath == null)
return;
final File finalDestinationFile = fs.file(finalDestinationPath);
final String relativePathForLogging = fs.path.relative(finalDestinationFile.path);
......@@ -100,7 +123,7 @@ class Template {
fileCount++;
finalDestinationFile.createSync(recursive: true);
final File sourceFile = fs.file(absoluteSrcPath);
final File sourceFile = fs.file(absoluteSourcePath);
// Step 2: If the absolute paths ends with a 'copy.tmpl', this file does
// not need mustache rendering but needs to be directly copied.
......
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withInputStream { stream ->
localProperties.load(stream)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 25
buildToolsVersion '25.0.3'
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "{{androidIdentifier}}"
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
androidTestCompile 'com.android.support:support-annotations:25.0.0'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
compile 'org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.2-4'
}
package {{androidIdentifier}}
import android.os.Bundle
import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity(): FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this);
}
}
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.2-4'
}
}
allprojects {
repositories {
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}
task wrapper(type: Wrapper) {
gradleVersion = '2.14.1'
}
......@@ -3,7 +3,7 @@
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$/android">
<sourceFolder url="file://$MODULE_DIR$/android/app/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/android/app/src/main/{{androidLanguage}}" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Android API {{androidSdkVersion}} Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
......
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
if ENV['FLUTTER_FRAMEWORK_DIR'] == nil
abort('Please set FLUTTER_FRAMEWORK_DIR to the directory containing Flutter.framework')
end
target 'Runner' do
# Pods for Runner
# Flutter Pods
pod 'Flutter', :path => ENV['FLUTTER_FRAMEWORK_DIR']
if File.exists? '../.flutter-plugins'
flutter_root = File.expand_path('..')
File.foreach('../.flutter-plugins') { |line|
plugin = line.split(pattern='=')
if plugin.length == 2
name = plugin[0].strip()
path = plugin[1].strip()
resolved_path = File.expand_path("#{path}/ios", flutter_root)
pod name, :path => resolved_path
else
puts "Invalid plugin specification: #{line}"
end
}
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end
......@@ -6,9 +6,7 @@ if ENV['FLUTTER_FRAMEWORK_DIR'] == nil
end
target 'Runner' do
# Uncomment this line if you want to use Swift in your app.
# Note that some Flutter plugins are not compatible with use_frameworks!
# use_frameworks!
use_frameworks!
# Pods for Runner
......
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
GeneratedPluginRegistrant.register(with: self);
return super.application(application, didFinishLaunchingWithOptions: launchOptions);
}
}
#import "GeneratedPluginRegistrant.h"
\ No newline at end of file
group '{{androidIdentifier}}'
version '1.0-SNAPSHOT'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
}
}
allprojects {
repositories {
jcenter()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 25
buildToolsVersion '25.0.3'
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
lintOptions {
disable 'InvalidPackage'
}
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.2-4'
}
package {{androidIdentifier}}
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.PluginRegistry.Registrar
class {{pluginClass}}(): MethodCallHandler {
companion object {
@JvmStatic
fun registerWith(registrar: Registrar): Unit {
val channel = MethodChannel(registrar.messenger(), "{{projectName}}")
channel.setMethodCallHandler({{pluginClass}}())
}
}
override fun onMethodCall(call: MethodCall, result: Result): Unit {
if (call.method.equals("getPlatformVersion")) {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
}
import Flutter
import UIKit
public class Swift{{pluginClass}}: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "{{projectName}}", binaryMessenger: registrar.messenger());
let instance = Swift{{pluginClass}}();
registrar.addMethodCallDelegate(instance, channel: channel);
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
result("iOS " + UIDevice.current.systemVersion);
}
}
\ No newline at end of file
#import <Flutter/Flutter.h>
@interface {{pluginClass}} : NSObject<FlutterPlugin>
@end
#import "{{pluginClass}}.h"
#import <{{projectName}}/{{projectName}}-Swift.h>
@implementation {{pluginClass}}
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[Swift{{pluginClass}} registerWithRegistrar:registrar];
}
@end
......@@ -10,6 +10,7 @@ void main() {
test('error', () async {
final BufferLogger mockLogger = new BufferLogger();
final VerboseLogger verboseLogger = new VerboseLogger(mockLogger);
verboseLogger.supportsColor = false;
verboseLogger.printStatus('Hey Hey Hey Hey');
verboseLogger.printTrace('Oooh, I do I do I do');
......
......@@ -39,15 +39,32 @@ void main() {
return _createAndAnalyzeProject(
projectDir,
<String>[],
fs.path.join(projectDir.path, 'lib', 'main.dart'),
<String>[
'android/app/src/main/java/com/yourcompany/flutter_project/MainActivity.java',
'ios/Runner/AppDelegate.h',
'ios/Runner/AppDelegate.m',
'ios/Runner/main.m',
'lib/main.dart',
],
);
});
testUsingContext('project with-driver-test', () async {
testUsingContext('kotlin/swift project', () async {
return _createAndAnalyzeProject(
projectDir,
<String>['--with-driver-test'],
fs.path.join(projectDir.path, 'lib', 'main.dart'),
<String>['--android-language', 'kotlin', '--ios-language', 'swift'],
<String>[
'android/app/src/main/kotlin/com/yourcompany/flutter_project/MainActivity.kt',
'ios/Runner/AppDelegate.swift',
'ios/Runner/Runner-Bridging-Header.h',
'lib/main.dart',
],
<String>[
'android/app/src/main/java/com/yourcompany/flutter_project/MainActivity.java',
'ios/Runner/AppDelegate.h',
'ios/Runner/AppDelegate.m',
'ios/Runner/main.m',
],
);
});
......@@ -55,7 +72,50 @@ void main() {
return _createAndAnalyzeProject(
projectDir,
<String>['--plugin'],
fs.path.join(projectDir.path, 'example', 'lib', 'main.dart'),
<String>[
'android/src/main/java/com/yourcompany/flutter_project/FlutterProjectPlugin.java',
'ios/Classes/FlutterProjectPlugin.h',
'ios/Classes/FlutterProjectPlugin.m',
'lib/flutter_project.dart',
'example/android/app/src/main/java/com/yourcompany/flutter_project_example/MainActivity.java',
'example/ios/Runner/AppDelegate.h',
'example/ios/Runner/AppDelegate.m',
'example/ios/Runner/main.m',
'example/lib/main.dart',
],
);
});
testUsingContext('kotlin/swift plugin project', () async {
return _createAndAnalyzeProject(
projectDir,
<String>['--plugin', '--android-language', 'kotlin', '--ios-language', 'swift'],
<String>[
'android/src/main/kotlin/com/yourcompany/flutter_project/FlutterProjectPlugin.kt',
'ios/Classes/FlutterProjectPlugin.h',
'ios/Classes/FlutterProjectPlugin.m',
'ios/Classes/SwiftFlutterProjectPlugin.swift',
'lib/flutter_project.dart',
'example/android/app/src/main/kotlin/com/yourcompany/flutter_project_example/MainActivity.kt',
'example/ios/Runner/AppDelegate.swift',
'example/ios/Runner/Runner-Bridging-Header.h',
'example/lib/main.dart',
],
<String>[
'android/src/main/java/com/yourcompany/flutter_project/FlutterProjectPlugin.java',
'example/android/app/src/main/java/com/yourcompany/flutter_project_example/MainActivity.java',
'example/ios/Runner/AppDelegate.h',
'example/ios/Runner/AppDelegate.m',
'example/ios/Runner/main.m',
],
);
});
testUsingContext('project with-driver-test', () async {
return _createAndAnalyzeProject(
projectDir,
<String>['--with-driver-test'],
<String>['lib/main.dart'],
);
});
......@@ -82,19 +142,16 @@ void main() {
<String>[file.path],
workingDirectory: projectDir.path,
);
final String formatted =
await process.stdout.transform(UTF8.decoder).join();
final String formatted = await process.stdout.transform(UTF8.decoder).join();
expect(original, formatted, reason: file.path);
}
}
// Generated Xcode settings
final String xcodeConfigPath =
fs.path.join('ios', 'Flutter', 'Generated.xcconfig');
final String xcodeConfigPath = fs.path.join('ios', 'Flutter', 'Generated.xcconfig');
expectExists(xcodeConfigPath);
final File xcodeConfigFile =
fs.file(fs.path.join(projectDir.path, xcodeConfigPath));
final File xcodeConfigFile = fs.file(fs.path.join(projectDir.path, xcodeConfigPath));
final String xcodeConfig = xcodeConfigFile.readAsStringSync();
expect(xcodeConfig, contains('FLUTTER_ROOT='));
expect(xcodeConfig, contains('FLUTTER_APPLICATION_PATH='));
......@@ -121,8 +178,8 @@ void main() {
final CommandRunner<Null> runner = createTestCommandRunner(command);
expect(
runner.run(<String>['create', projectDir.path, '--pub']),
throwsToolExit(exitCode: 2, message: 'Try moving --pub')
runner.run(<String>['create', projectDir.path, '--pub']),
throwsToolExit(exitCode: 2, message: 'Try moving --pub'),
);
});
......@@ -135,8 +192,8 @@ void main() {
if (!existingFile.existsSync())
existingFile.createSync(recursive: true);
expect(
runner.run(<String>['create', existingFile.path]),
throwsToolExit(message: 'file exists')
runner.run(<String>['create', existingFile.path]),
throwsToolExit(message: 'file exists'),
);
});
......@@ -145,18 +202,16 @@ void main() {
final CreateCommand command = new CreateCommand();
final CommandRunner<Null> runner = createTestCommandRunner(command);
expect(
runner.run(<String>['create', fs.path.join(projectDir.path, 'invalidName')]),
throwsToolExit(message: '"invalidName" is not a valid Dart package name.')
runner.run(<String>['create', fs.path.join(projectDir.path, 'invalidName')]),
throwsToolExit(message: '"invalidName" is not a valid Dart package name.'),
);
});
});
}
Future<Null> _createAndAnalyzeProject(
Directory dir,
List<String> createArgs,
String mainPath,
) async {
Directory dir, List<String> createArgs, List<String> expectedPaths,
[List<String> unexpectedPaths = const <String>[]]) async {
Cache.flutterRoot = '../..';
final CreateCommand command = new CreateCommand();
final CommandRunner<Null> runner = createTestCommandRunner(command);
......@@ -165,7 +220,12 @@ Future<Null> _createAndAnalyzeProject(
args.add(dir.path);
await runner.run(args);
expect(fs.file(mainPath).existsSync(), true);
for (String path in expectedPaths) {
expect(fs.file(fs.path.join(dir.path, path)).existsSync(), true, reason: '$path does not exist');
}
for (String path in unexpectedPaths) {
expect(fs.file(fs.path.join(dir.path, path)).existsSync(), false, reason: '$path exists');
}
final String flutterToolsPath = fs.path.absolute(fs.path.join(
'bin',
'flutter_tools.dart',
......
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