Plugin for Plugin, или расширяем Android New Build System. Антон Руткевич

Preview:

DESCRIPTION

Plugin for Plugin, или расширяем Android New Build System

Citation preview

Plugin for plugin, or extending Android New Build System Anton Rutkevich

About me

›  4+ years of Android development

›  Mobile game-dev experience

›  At Yandex:

1.  Mobile Yandex.Metrica

2.  Continuous Integration

The image cannot be displayed. Your computer may not have enough memory to open the image, or the image may have been corrupted. Restart your computer, and then open the file again. If the red x still appears, you may have to delete the image and then insert it again.

Intro

Why do I need this?

What can be done?

›  Additional resources/code/manifest processing

›  Output processing (apk, aar, jar)

›  Other things

Flavors 2 Flavors!

Story

Prod / test servers Logs on/off Test / prod analytics Ads on/off Unique build number! ...

I want to configure it myself!

3 Flavors? Hmm…

Story Prod / test

servers Flavors!

Logs on/off Test / prod analytics Ads on/off Unique build

number! ...

I want to configure it

myself!

2 Flavors! 3 Flavors? Hmm...

Android dev Manager

How should it work

Java code build.gradle Teamcity

Actual value: "https://my.server.com"

CI server can do it

Our job

Insert CI value into BuildConfig.java

// app/build.gradle apply plugin: 'com.android.application' android { � defaultConfig { buildConfigField "String", "URL", "\"${teamcity['server-url']}\"" buildConfigField "String", "URL", "\"#server-url\"" } �} project.teamcity = [

"server-url" : "https://my.server.com" // ... �]

buildConfigField "String", "URL", "\"${teamcity['server-url']}\"".

Use BuildConfig.java from Java

public class SomeJavaClass { � // ... public static final String SERVER_URL = "https://my.server.com"; public static final String SERVER_URL = BuildConfig.URL; // ... }

public static final String SERVER_URL = "https://my.server.com";

BuildConfig placeholder plugin

›  Replaces placeholder values with values from some map

›  Map can come from anywhere

Goal

// app/build.gradle apply plugin: 'com.android.application' apply plugin: 'placeholder' �placeholder { � replacements = project.teamcity } android { � defaultConfig { � buildConfigField "String", "URL", "\"#server-url\"" � } �}

Table of contents

›  Gradle basics

›  New Build System workflow

›  Hello, Gradle plugin!

›  Extending Android New Build System

The image cannot be displayed. Your computer may not have enough memory to open the image, or the image may have been corrupted. Restart your computer, and then open the file again. If the red x still appears, you may have to delete the image and then insert it again.

Gradle basics

Tools we will use

Plugins everywhere

NBS

Tasks

›  Can be configured with { }

›  Consist of actions

›  Can depend on other tasks

›  Can have inputs / outputs

Task consists of actions

Action Action

Action Action

doFirst() doLast(), or <<

Task

Tasks execution order

Task 2

Task 3

Task 4

Execution order

dependsOn

Task 1

Task 2 Task 3 Task 4 Task 1

dependsOn

dependsOn

Outputs

Task inputs / outputs

Task Inputs

Inputs & outputs did not change =>

UP-TO-DATE

Task example

task myTask { ext.myMessage = "hello" } myTask << { println myMessage } task otherTask(dependsOn: myTask)

Task example output

>> gradle otherTask :app:myTask hello :app:otherTask

Build lifecycle

Initialization Configuration Execution

settings.gradle

Projects creation Projects configuration Tasks creation Tasks configuration project.afterEvaluate { }

Task graph execution

Task graph

build.gradle

Build initialization

The image cannot be displayed. Your computer may not have enough memory to open the image, or the image may have been corrupted. Restart your computer, and then open the file again. If the red x still appears, you may have to delete the image and then insert it again.

The New Build System workflow

What's so special?

What tasks will be launched?

build

check assemble

assembleDebug assembleRelease

assemble<VariantName> Guaranteed

Android project build overview

Tasks we will need

assemble<VariantName>

generate<VariantName>BuildConfig

compile<VariantName>Java

....

....

....

Variant API

Source code is your documentation!

›  Access to most variant's tasks

›  Variant output related properties

›  Different for apps & libraries

The image cannot be displayed. Your computer may not have enough memory to open the image, or the image may have been corrupted. Restart your computer, and then open the file again. If the red x still appears, you may have to delete the image and then insert it again.

Hello, Gradle plugin!

The first steps

The very basic one

src/main/groovy/com/example/gradle/PlaceholderPlugin.gradle

public class PlaceholderPlugin implements Plugin<Project> { @Override � void apply(Project project) { project.task('hello') << { � println "Hello Gradle plugin!" } } }

Bind plugin class to plugin name

src/main/resources/META-INF/gradle-plugins/placeholder.properties

implementation-class=com.example.gradle.PlaceholderPlugin

Extension

Extension

Add extension

src/main/groovy/com/example/gradle/PlaceholderExtension.gradle

class PlaceholderExtension { � def replacements = [:] } �

Add extension

@Override �void apply(Project project) { � project.task('hello') << { println "Hello Gradle plugin!" println "Hi, ${project.placeholder.replacements}" } } �

PlaceholderExtension extension = project.extensions.create( � "placeholder", PlaceholderExtension�);

println "Hello Gradle plugin!"

Use extension

// app/build.gradle apply plugin: 'placeholder' ��placeholder { � replacements = ["server-url" : "https://my.server.com"] } �--------------------------------------------- >> gradle hello :app:hello�Hi, [server-url:https://my.server.com]

The image cannot be displayed. Your computer may not have enough memory to open the image, or the image may have been corrupted. Restart your computer, and then open the file again. If the red x still appears, you may have to delete the image and then insert it again.

Extending The New Build System

Let's do it!

Check for New Build System

// PlaceholderPlugin.groovy @Override �void apply(Project project) { if (project.hasProperty("android")) { PlaceholderExtension extension = project.extensions.create( � "placeholder", PlaceholderExtension� ); def android = project.android // all code goes here } } �

Let the New Build System do its job

// PlaceholderPlugin.apply() if (project.hasProperty("android")) { PlaceholderExtension extension = project.extensions.create( � "placeholder", PlaceholderExtension� ); def android = project.android project.afterEvaluate { � // at this point we have all // tasks from New Build System } }

Process every variant

// PlaceholderPlugin.apply() project.afterEvaluate { if (android.hasProperty('applicationVariants')) { android.applicationVariants.all { variant -> addActions(project, variant, extension) } } else if (android.hasProperty('libraryVariants')) { android.libraryVariants.all { variant -> addActions(project, variant, extension) } } }

Add task

// PlaceholderPlugin.groovy def addActions(Project project, variant, PlaceholderExtension extension) { Task processPlaceholders = project.task( "process${variant.name.capitalize()}Placeholders" ) processPlaceholders << { println "I will replace ${variant.name}!" } }

Insert task into build process assemble<VariantName>

generate<VariantName>BuildConfig

compile<VariantName>Java

....

....

....

process<VariantName>Placeholders

Insert task into build process

// PlaceholderPlugin.groovy def addActions(Project project, variant, PlaceholderExtension extension) { Task processPlaceholders = ... ... variant.javaCompile.dependsOn processPlaceholders� processPlaceholders.dependsOn variant.generateBuildConfig }

Does it really work?

>> gradle assembleDebug :app:preBuild ... :app:generateDebugBuildConfig ... :app:processDebugPlaceholders I will replace debug! :app:compileDebugJava ...

Actual work. Perform replacements

// PlaceholderPlugin.addActions() processPlaceholders << { def buildConfigFile = getBuildConfig(variant) � extension.replacements.each { replacement -> � project.ant.replace( � file: buildConfigFile, � token: "#${replacement.key}", value: replacement.value � ) � } }

Handling inputs / outputs

process Placeholders

BuildConfig.java BuildConfig.java

replacements

generate BuildConfig

...

replacements << Action

BuildConfig.java

Replace task with 'doLast'

// PlaceholderPlugin.addActions() processPlaceholders << { variant.generateBuildConfig << { def buildConfigFile = ... extension.replacements.each { ... } } variant.generateBuildConfig.inputs.property( "replacements", extension.replacements )

processPlaceholders << {

│ We've done it!

Remember how to use it?

// app/build.gradle apply plugin: 'com.android.application' �apply plugin: 'placeholder' ��placeholder { � replacements = project.teamcity } ��android { � defaultConfig { � buildConfigField "String", "URL", "\"#server-url\"" � } �}

Does it work?

app/build/generated/source/buildConfig/debug/com/example/sample/BuildConfig.java

public final class BuildConfig { � // Fields from default config. � public static final String URL = "https://my.server.com"; }

The image cannot be displayed. Your computer may not have enough memory to open the image, or the image may have been corrupted. Restart your computer, and then open the file again. If the red x still appears, you may have to delete the image and then insert it again.

To summarize

Key steps

›  Create Gradle plugin

›  Inside afterEvaluate { }

›  Process every variant

›  Add action to generateBuildConfig

›  Handle inputs / outputs

What's next?

›  Default values support

›  Errors handling

›  Publish ( jcenter / mavenCentral / other )

Links

Gradle http://www.gradle.org/

The New Build System http://tools.android.com/tech-docs/new-build-system http://tools.android.com/tech-docs/new-build-system/build-workflow

Github sample https://github.com/roottony/android-placeholder-plugin

Thank you for your attention!

anton.rutkevich@gmail.com

Anton Rutkevich

Senior software engineer

antonrut@yandex-team.ru