Commit f34f8a31 authored by Jakob Andersen's avatar Jakob Andersen Committed by GitHub

Add template for plugin projects. (#9076)

Plugin projects are created by running `flutter create --plugin <name>`.

An example app is also created in the plugin project, using the normal 'create' template, which has been modified to allow for conditional plugin code.

Modified the android package name to match package naming conventions (all lower-case, and must match the directory name).
parent 471c97df
...@@ -30,6 +30,12 @@ class CreateCommand extends FlutterCommand { ...@@ -30,6 +30,12 @@ class CreateCommand extends FlutterCommand {
defaultsTo: false, defaultsTo: false,
help: 'Also add a flutter_driver dependency and generate a sample \'flutter drive\' test.' help: 'Also add a flutter_driver dependency and generate a sample \'flutter drive\' test.'
); );
argParser.addFlag(
'plugin',
negatable: true,
defaultsTo: false,
help: 'Generate a new Flutter Plugin project.'
);
argParser.addOption( argParser.addOption(
'description', 'description',
defaultsTo: 'A new flutter project.', defaultsTo: 'A new flutter project.',
...@@ -80,9 +86,10 @@ class CreateCommand extends FlutterCommand { ...@@ -80,9 +86,10 @@ class CreateCommand extends FlutterCommand {
if (!fs.isFileSync(fs.path.join(flutterDriverPackagePath, 'pubspec.yaml'))) if (!fs.isFileSync(fs.path.join(flutterDriverPackagePath, 'pubspec.yaml')))
throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2); throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2);
final bool generatePlugin = argResults['plugin'];
final Directory projectDir = fs.directory(argResults.rest.first); final Directory projectDir = fs.directory(argResults.rest.first);
final String dirPath = fs.path.normalize(projectDir.absolute.path); final String dirPath = fs.path.normalize(projectDir.absolute.path);
final String relativePath = fs.path.relative(dirPath);
final String projectName = _normalizeProjectName(fs.path.basename(dirPath)); final String projectName = _normalizeProjectName(fs.path.basename(dirPath));
String error =_validateProjectDir(dirPath, flutterRoot: flutterRoot); String error =_validateProjectDir(dirPath, flutterRoot: flutterRoot);
...@@ -93,24 +100,55 @@ class CreateCommand extends FlutterCommand { ...@@ -93,24 +100,55 @@ class CreateCommand extends FlutterCommand {
if (error != null) if (error != null)
throwToolExit(error); throwToolExit(error);
final int generatedCount = _renderTemplates( final Map<String, dynamic> templateContext = _templateContext(
projectName, projectName, argResults['description'], dirPath,
argResults['description'], flutterPackagesDirectory, renderDriverTest: argResults['with-driver-test'],
dirPath, withPluginHook: generatePlugin,
flutterPackagesDirectory,
renderDriverTest: argResults['with-driver-test']
); );
printStatus('Creating project ${fs.path.relative(dirPath)}...');
int generatedCount = 0;
String appPath = dirPath;
if (generatePlugin) {
final String description = argResults.wasParsed('description')
? argResults['description']
: 'A new flutter plugin project.';
templateContext['description'] = description;
generatedCount += _renderTemplate('plugin', dirPath, templateContext);
if (argResults['pub'])
await pubGet(directory: dirPath);
appPath = fs.path.join(dirPath, 'example');
final String androidPluginIdentifier = templateContext['androidIdentifier'];
final String exampleProjectName = projectName + '_example';
templateContext['projectName'] = exampleProjectName;
templateContext['androidIdentifier'] = _createAndroidIdentifier(exampleProjectName);
templateContext['iosIdentifier'] = _createUTIIdentifier(exampleProjectName);
templateContext['description'] = 'Demonstrates how to use the $projectName plugin.';
templateContext['pluginProjectName'] = projectName;
templateContext['androidPluginIdentifier'] = androidPluginIdentifier;
}
generatedCount += _renderTemplate('create', appPath, templateContext);
if (argResults['with-driver-test']) {
final String testPath = fs.path.join(appPath, 'test_driver');
generatedCount += _renderTemplate('driver', testPath, templateContext);
}
printStatus('Wrote $generatedCount files.'); printStatus('Wrote $generatedCount files.');
printStatus(''); printStatus('');
updateXcodeGeneratedProperties(dirPath, BuildMode.debug, flx.defaultMainPath); updateXcodeGeneratedProperties(appPath, BuildMode.debug, flx.defaultMainPath);
if (argResults['pub']) if (argResults['pub'])
await pubGet(directory: dirPath); await pubGet(directory: appPath);
printStatus(''); printStatus('');
// Run doctor; tell the user the next steps. // Run doctor; tell the user the next steps.
final String relativeAppPath = fs.path.relative(appPath);
final String relativePluginPath = fs.path.relative(dirPath);
if (doctor.canLaunchAnything) { if (doctor.canLaunchAnything) {
// Let them know a summary of the state of their tooling. // Let them know a summary of the state of their tooling.
await doctor.summary(); await doctor.summary();
...@@ -118,11 +156,18 @@ class CreateCommand extends FlutterCommand { ...@@ -118,11 +156,18 @@ class CreateCommand extends FlutterCommand {
printStatus(''' printStatus('''
All done! In order to run your application, type: All done! In order to run your application, type:
\$ cd $relativePath \$ cd $relativeAppPath
\$ flutter run \$ flutter run
Your main program file is lib/main.dart in the $relativePath directory. Your main program file is lib/main.dart in the $relativeAppPath directory.
''');
if (generatePlugin) {
printStatus('''
Your plugin code is in lib/$projectName.dart in the $relativePluginPath directory.
Host platform code is in the android/ and ios/ directories under $relativePluginPath.
'''); ''');
}
} else { } else {
printStatus("You'll need to install additional components before you can run " printStatus("You'll need to install additional components before you can run "
"your Flutter app:"); "your Flutter app:");
...@@ -133,48 +178,40 @@ Your main program file is lib/main.dart in the $relativePath directory. ...@@ -133,48 +178,40 @@ Your main program file is lib/main.dart in the $relativePath directory.
printStatus(''); printStatus('');
printStatus("After installing components, run 'flutter doctor' in order to " printStatus("After installing components, run 'flutter doctor' in order to "
"re-validate your setup."); "re-validate your setup.");
printStatus("When complete, type 'flutter run' from the '$relativePath' " printStatus("When complete, type 'flutter run' from the '$relativeAppPath' "
"directory in order to launch your app."); "directory in order to launch your app.");
printStatus("Your main program file is: $relativePath/lib/main.dart"); printStatus("Your main program file is: $relativeAppPath/lib/main.dart");
} }
} }
int _renderTemplates(String projectName, String projectDescription, String dirPath, Map<String, dynamic> _templateContext(String projectName,
String flutterPackagesDirectory, { bool renderDriverTest: false }) { String projectDescription, String dirPath, String flutterPackagesDirectory,
fs.directory(dirPath).createSync(recursive: true); { bool renderDriverTest: false, bool withPluginHook: false }) {
flutterPackagesDirectory = fs.path.normalize(flutterPackagesDirectory); flutterPackagesDirectory = fs.path.normalize(flutterPackagesDirectory);
flutterPackagesDirectory = _relativePath(from: dirPath, to: flutterPackagesDirectory); flutterPackagesDirectory = _relativePath(from: dirPath, to: flutterPackagesDirectory);
printStatus('Creating project ${fs.path.relative(dirPath)}...'); final String pluginDartClass = _createPluginClassName(projectName);
final String pluginClass = pluginDartClass.endsWith('Plugin')
? pluginDartClass
: pluginDartClass + 'Plugin';
final Map<String, dynamic> templateContext = <String, dynamic>{ return <String, dynamic>{
'projectName': projectName, 'projectName': projectName,
'androidIdentifier': _createAndroidIdentifier(projectName), 'androidIdentifier': _createAndroidIdentifier(projectName),
'iosIdentifier': _createUTIIdentifier(projectName), 'iosIdentifier': _createUTIIdentifier(projectName),
'description': projectDescription, 'description': projectDescription,
'flutterPackagesDirectory': flutterPackagesDirectory, 'flutterPackagesDirectory': flutterPackagesDirectory,
'androidMinApiLevel': android.minApiLevel 'androidMinApiLevel': android.minApiLevel,
'withDriverTest': renderDriverTest,
'pluginClass': pluginClass,
'pluginDartClass': pluginDartClass,
'withPluginHook': withPluginHook,
}; };
}
int fileCount = 0; int _renderTemplate(String templateName, String dirPath, Map<String, dynamic> context) {
final Template template = new Template.fromName(templateName);
templateContext['withDriverTest'] = renderDriverTest; return template.render(fs.directory(dirPath), context, overwriteExisting: false);
final Template createTemplate = new Template.fromName('create');
fileCount += createTemplate.render(
fs.directory(dirPath),
templateContext, overwriteExisting: false,
projectName: projectName
);
if (renderDriverTest) {
final Template driverTemplate = new Template.fromName('driver');
fileCount += driverTemplate.render(fs.directory(fs.path.join(dirPath, 'test_driver')),
templateContext, overwriteExisting: false);
}
return fileCount;
} }
} }
...@@ -187,7 +224,12 @@ String _normalizeProjectName(String name) { ...@@ -187,7 +224,12 @@ String _normalizeProjectName(String name) {
} }
String _createAndroidIdentifier(String name) { String _createAndroidIdentifier(String name) {
return 'com.yourcompany.${camelCase(name)}'; return 'com.yourcompany.$name';
}
String _createPluginClassName(String name) {
final String camelizedName = camelCase(name);
return camelizedName[0].toUpperCase() + camelizedName.substring(1);
} }
String _createUTIIdentifier(String name) { String _createUTIIdentifier(String name) {
......
...@@ -62,11 +62,12 @@ class Template { ...@@ -62,11 +62,12 @@ class Template {
Directory destination, Directory destination,
Map<String, dynamic> context, { Map<String, dynamic> context, {
bool overwriteExisting: true, bool overwriteExisting: true,
String projectName
}) { }) {
destination.createSync(recursive: true); destination.createSync(recursive: true);
int fileCount = 0; int fileCount = 0;
final String projectName = context['projectName'];
final String pluginClass = context['pluginClass'];
final String destinationDirPath = destination.absolute.path; final String destinationDirPath = destination.absolute.path;
_templateFilePaths.forEach((String relativeDestPath, String absoluteSrcPath) { _templateFilePaths.forEach((String relativeDestPath, String absoluteSrcPath) {
...@@ -76,6 +77,8 @@ class Template { ...@@ -76,6 +77,8 @@ class Template {
.replaceAll(_kTemplateExtension, ''); .replaceAll(_kTemplateExtension, '');
if (projectName != null) if (projectName != null)
finalDestinationPath = finalDestinationPath.replaceAll('projectName', projectName); finalDestinationPath = finalDestinationPath.replaceAll('projectName', projectName);
if (pluginClass != null)
finalDestinationPath = finalDestinationPath.replaceAll('pluginClass', pluginClass);
final File finalDestinationFile = fs.file(finalDestinationPath); final File finalDestinationFile = fs.file(finalDestinationPath);
final String relativePathForLogging = fs.path.relative(finalDestinationFile.path); final String relativePathForLogging = fs.path.relative(finalDestinationFile.path);
......
...@@ -2,11 +2,16 @@ package {{androidIdentifier}}; ...@@ -2,11 +2,16 @@ package {{androidIdentifier}};
import android.os.Bundle; import android.os.Bundle;
import io.flutter.app.FlutterActivity; import io.flutter.app.FlutterActivity;
{{#withPluginHook}}
import {{androidPluginIdentifier}}.{{pluginClass}};
{{/withPluginHook}}
public class MainActivity extends FlutterActivity { public class MainActivity extends FlutterActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
{{#withPluginHook}}
{{pluginClass}}.register(this);
{{/withPluginHook}}
} }
} }
#include "AppDelegate.h" #include "AppDelegate.h"
{{#withPluginHook}}
#include "{{pluginClass}}.h"
@implementation AppDelegate {
{{pluginClass}} *_{{pluginProjectName}};
}
{{/withPluginHook}}
{{^withPluginHook}}
@implementation AppDelegate @implementation AppDelegate
{{/withPluginHook}}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch. // Override point for customization after application launch.
{{#withPluginHook}}
FlutterViewController *flutterController =
(FlutterViewController *)self.window.rootViewController;
_{{pluginProjectName}} = [[{{pluginClass}} alloc] initWithFlutterView:flutterController];
{{/withPluginHook}}
return YES; return YES;
} }
......
...@@ -2,6 +2,9 @@ import 'package:flutter/material.dart'; ...@@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
{{#withDriverTest}} {{#withDriverTest}}
import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_driver/driver_extension.dart';
{{/withDriverTest}} {{/withDriverTest}}
{{#withPluginHook}}
import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart';
{{/withPluginHook}}
void main() { void main() {
{{#withDriverTest}} {{#withDriverTest}}
...@@ -55,6 +58,19 @@ class MyHomePage extends StatefulWidget { ...@@ -55,6 +58,19 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
int _counter = 0; int _counter = 0;
{{#withPluginHook}}
String _platformVersion = 'Unknown';
@override
void initState() {
super.initState();
{{pluginDartClass}}.platformVersion.then((String platformVersion) {
setState(() {
_platformVersion = platformVersion;
});
});
}
{{/withPluginHook}}
void _incrementCounter() { void _incrementCounter() {
setState(() { setState(() {
...@@ -84,9 +100,21 @@ class _MyHomePageState extends State<MyHomePage> { ...@@ -84,9 +100,21 @@ class _MyHomePageState extends State<MyHomePage> {
title: new Text(config.title), title: new Text(config.title),
), ),
body: new Center( body: new Center(
{{#withPluginHook}}
child: new Column(
mainAxisSize: MainAxisSize.min,
children: [
new Text('Running on: $_platformVersion\n'),
new Text(
'Button tapped $_counter time${ _counter == 1 ? '' : 's' }.'),
],
),
{{/withPluginHook}}
{{^withPluginHook}}
child: new Text( child: new Text(
'Button tapped $_counter time${ _counter == 1 ? '' : 's' }.', 'Button tapped $_counter time${ _counter == 1 ? '' : 's' }.',
), ),
{{/withPluginHook}}
), ),
floatingActionButton: new FloatingActionButton( floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter, onPressed: _incrementCounter,
......
...@@ -9,6 +9,10 @@ dev_dependencies: ...@@ -9,6 +9,10 @@ dev_dependencies:
flutter_driver: flutter_driver:
sdk: flutter sdk: flutter
{{/withDriverTest}} {{/withDriverTest}}
{{#withPluginHook}}
{{pluginProjectName}}:
path: ../
{{/withPluginHook}}
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec # following page: https://www.dartlang.org/tools/pub/pubspec
......
.DS_Store
.atom/
.idea
.packages
.pub/
build/
ios/.generated/
packages
pubspec.lock
// Copyright 2017 Your Company. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Your Company nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# {{projectName}}
{{description}}
## Getting Started
For help getting started with Flutter, view our online
[documentation](http://flutter.io/).
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
/gradle
/gradlew
/gradlew.bat
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'
android {
compileSdkVersion 25
buildToolsVersion '25.0.0'
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
lintOptions {
disable 'InvalidPackage'
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="{{androidIdentifier}}"
android:versionCode="1"
android:versionName="0.0.1">
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" />
</manifest>
package {{androidIdentifier}};
import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.FlutterMethodChannel;
import io.flutter.plugin.common.FlutterMethodChannel.MethodCallHandler;
import io.flutter.plugin.common.FlutterMethodChannel.Response;
import io.flutter.plugin.common.MethodCall;
import java.util.HashMap;
import java.util.Map;
/**
* {{pluginClass}}
*/
public class {{pluginClass}} implements MethodCallHandler {
private FlutterActivity activity;
public static void register(FlutterActivity activity) {
new {{pluginClass}}(activity);
}
private {{pluginClass}}(FlutterActivity activity) {
this.activity = activity;
new FlutterMethodChannel(activity.getFlutterView(), "{{projectName}}").setMethodCallHandler(this);
}
@Override
public void onMethodCall(MethodCall call, Response response) {
if (call.method.equals("getPlatformVersion")) {
response.success("Android " + android.os.Build.VERSION.RELEASE);
} else {
throw new IllegalArgumentException("Unknown method " + call.method);
}
}
}
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
#import <Flutter/Flutter.h>
@interface {{pluginClass}} : NSObject
- initWithFlutterView:(FlutterViewController *)flutterView;
@end
#import "{{pluginClass}}.h"
@implementation {{pluginClass}} {
}
- (instancetype)initWithFlutterView:(FlutterViewController *)flutterView {
self = [super init];
if (self) {
FlutterMethodChannel *channel = [FlutterMethodChannel
methodChannelNamed:@"{{projectName}}"
binaryMessenger:flutterView
codec:[FlutterStandardMethodCodec sharedInstance]];
[channel setMethodCallHandler:^(FlutterMethodCall *call,
FlutterResultReceiver result) {
if ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice]
systemVersion]],
nil);
}
}];
}
return self;
}
@end
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = '{{projectName}}'
s.version = '0.0.1'
s.summary = '{{description}}'
s.description = <<-DESC
{{description}}
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter'
end
import 'dart:async';
import 'package:flutter/services.dart';
class {{pluginDartClass}} {
static const PlatformMethodChannel _channel =
const PlatformMethodChannel('{{projectName}}');
static Future<String> get platformVersion =>
_channel.invokeMethod('getPlatformVersion');
}
name: {{projectName}}
description: {{description}}
flutter:
plugin:
dependencies:
flutter:
sdk: flutter
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