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 {
defaultsTo: false,
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(
'description',
defaultsTo: 'A new flutter project.',
......@@ -80,9 +86,10 @@ class CreateCommand extends FlutterCommand {
if (!fs.isFileSync(fs.path.join(flutterDriverPackagePath, 'pubspec.yaml')))
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 String dirPath = fs.path.normalize(projectDir.absolute.path);
final String relativePath = fs.path.relative(dirPath);
final String projectName = _normalizeProjectName(fs.path.basename(dirPath));
String error =_validateProjectDir(dirPath, flutterRoot: flutterRoot);
......@@ -93,24 +100,55 @@ class CreateCommand extends FlutterCommand {
if (error != null)
throwToolExit(error);
final int generatedCount = _renderTemplates(
projectName,
argResults['description'],
dirPath,
flutterPackagesDirectory,
renderDriverTest: argResults['with-driver-test']
final Map<String, dynamic> templateContext = _templateContext(
projectName, argResults['description'], dirPath,
flutterPackagesDirectory, renderDriverTest: argResults['with-driver-test'],
withPluginHook: generatePlugin,
);
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('');
updateXcodeGeneratedProperties(dirPath, BuildMode.debug, flx.defaultMainPath);
updateXcodeGeneratedProperties(appPath, BuildMode.debug, flx.defaultMainPath);
if (argResults['pub'])
await pubGet(directory: dirPath);
await pubGet(directory: appPath);
printStatus('');
// 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) {
// Let them know a summary of the state of their tooling.
await doctor.summary();
......@@ -118,11 +156,18 @@ class CreateCommand extends FlutterCommand {
printStatus('''
All done! In order to run your application, type:
\$ cd $relativePath
\$ cd $relativeAppPath
\$ 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 {
printStatus("You'll need to install additional components before you can run "
"your Flutter app:");
......@@ -133,48 +178,40 @@ Your main program file is lib/main.dart in the $relativePath directory.
printStatus('');
printStatus("After installing components, run 'flutter doctor' in order to "
"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.");
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,
String flutterPackagesDirectory, { bool renderDriverTest: false }) {
fs.directory(dirPath).createSync(recursive: true);
Map<String, dynamic> _templateContext(String projectName,
String projectDescription, String dirPath, String flutterPackagesDirectory,
{ bool renderDriverTest: false, bool withPluginHook: false }) {
flutterPackagesDirectory = fs.path.normalize(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,
'androidIdentifier': _createAndroidIdentifier(projectName),
'iosIdentifier': _createUTIIdentifier(projectName),
'description': projectDescription,
'flutterPackagesDirectory': flutterPackagesDirectory,
'androidMinApiLevel': android.minApiLevel
'androidMinApiLevel': android.minApiLevel,
'withDriverTest': renderDriverTest,
'pluginClass': pluginClass,
'pluginDartClass': pluginDartClass,
'withPluginHook': withPluginHook,
};
}
int fileCount = 0;
templateContext['withDriverTest'] = renderDriverTest;
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;
int _renderTemplate(String templateName, String dirPath, Map<String, dynamic> context) {
final Template template = new Template.fromName(templateName);
return template.render(fs.directory(dirPath), context, overwriteExisting: false);
}
}
......@@ -187,7 +224,12 @@ String _normalizeProjectName(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) {
......
......@@ -62,11 +62,12 @@ class Template {
Directory destination,
Map<String, dynamic> context, {
bool overwriteExisting: true,
String projectName
}) {
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) {
......@@ -76,6 +77,8 @@ class Template {
.replaceAll(_kTemplateExtension, '');
if (projectName != null)
finalDestinationPath = finalDestinationPath.replaceAll('projectName', projectName);
if (pluginClass != null)
finalDestinationPath = finalDestinationPath.replaceAll('pluginClass', pluginClass);
final File finalDestinationFile = fs.file(finalDestinationPath);
final String relativePathForLogging = fs.path.relative(finalDestinationFile.path);
......
......@@ -2,11 +2,16 @@ package {{androidIdentifier}};
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
{{#withPluginHook}}
import {{androidPluginIdentifier}}.{{pluginClass}};
{{/withPluginHook}}
public class MainActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
{{#withPluginHook}}
{{pluginClass}}.register(this);
{{/withPluginHook}}
}
}
#include "AppDelegate.h"
{{#withPluginHook}}
#include "{{pluginClass}}.h"
@implementation AppDelegate {
{{pluginClass}} *_{{pluginProjectName}};
}
{{/withPluginHook}}
{{^withPluginHook}}
@implementation AppDelegate
{{/withPluginHook}}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
{{#withPluginHook}}
FlutterViewController *flutterController =
(FlutterViewController *)self.window.rootViewController;
_{{pluginProjectName}} = [[{{pluginClass}} alloc] initWithFlutterView:flutterController];
{{/withPluginHook}}
return YES;
}
......
......@@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
{{#withDriverTest}}
import 'package:flutter_driver/driver_extension.dart';
{{/withDriverTest}}
{{#withPluginHook}}
import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart';
{{/withPluginHook}}
void main() {
{{#withDriverTest}}
......@@ -55,6 +58,19 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
{{#withPluginHook}}
String _platformVersion = 'Unknown';
@override
void initState() {
super.initState();
{{pluginDartClass}}.platformVersion.then((String platformVersion) {
setState(() {
_platformVersion = platformVersion;
});
});
}
{{/withPluginHook}}
void _incrementCounter() {
setState(() {
......@@ -84,9 +100,21 @@ class _MyHomePageState extends State<MyHomePage> {
title: new Text(config.title),
),
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(
'Button tapped $_counter time${ _counter == 1 ? '' : 's' }.',
),
{{/withPluginHook}}
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
......
......@@ -9,6 +9,10 @@ dev_dependencies:
flutter_driver:
sdk: flutter
{{/withDriverTest}}
{{#withPluginHook}}
{{pluginProjectName}}:
path: ../
{{/withPluginHook}}
# For information on the generic Dart part of this file, see the
# 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