Android
Kotlin
gradle

build.gradle.ktsへの書き換えとハマリポイント part 1

More than 1 year has passed since last update.

はじめに

KotlinConfで build.gradle をktsで書いてバリバリ補完効かせながら書いてるのを見てよさそうじゃんって試してみたらめちゃめちゃハマったのでメモです。

環境

  • Android Studio 3.0
    • Gradle wrapper 4.3.1
  • Mac OS Sierra

サンプルプロジェクト

こんな感じのサンプルを書き換えていきます。

build.gradle
buildscript {
    ext.kotlin_version = '1.2.0-rc-39'
    repositories {
        google()
        jcenter()
        maven { url 'http://dl.bintray.com/kotlin/kotlin-eap-1.2' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.google.gms:google-services:3.1.2'
        classpath 'com.github.gfx.ribbonizer:ribbonizer-plugin:2.1.0'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven { url 'http://dl.bintray.com/kotlin/kotlin-eap-1.2' }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.github.gfx.ribbonizer'

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.chibatching.ktssample"
        minSdkVersion 17
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        debug {
            minifyEnabled false
            applicationIdSuffix '.debug'
        }
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

ribbonizer {
    builder { variant, iconFile ->
        if (variant.buildType.name == "debug") {
            return yellowRibbonFilter(variant, iconFile)
        } else {
            return grayRibbonFilter(variant, iconFile)
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"

    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'

    implementation 'com.google.firebase:firebase-core:11.6.0'

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

apply plugin: 'com.google.gms.google-services'

Step 1 - ルートレベルのbuild.gradle

分量的にも難易度的にもとりあえずここからやるのがいいと思います。

素直に変換すると次のような感じになります。ほとんど同じですね。
大きく違うのはgroovyの時に ext.kotlin_version = '1.2.0-rc-39' としていた箇所。
delegated propertyを使うことで(gradleプロジェクトの)拡張プロパティを定義することができます(ついでに名前をキャメルケースに変更したのでappレベルのほうも適宜変更してください)。

build.gradle.kts
buildscript {
    val kotlinVersion by extra { "1.2.0-rc-39" }
    repositories {
        google()
        jcenter()
        maven(url = "http://dl.bintray.com/kotlin/kotlin-eap-1.2")
    }
    dependencies {
        classpath("com.android.tools.build:gradle:3.0.0")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
        classpath("com.google.gms:google-services:3.1.2")
        classpath("com.github.gfx.ribbonizer:ribbonizer-plugin:2.1.0")
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven(url = "http://dl.bintray.com/kotlin/kotlin-eap-1.2")
    }
}

task("clean", Delete::class) {
    delete = setOf(rootProject.buildDir)
}

ハマリポイント 1: gradle syncできない

ここで一発でgradle syncできたあなた!ラッキーですね!
僕は出来ませんでした。一番ハマったのがここです。
しかも、./gradlew コマンドからはビルドできるのにAndroid Studioではsyncできないという状況。
KotlinConf会場のGradleブースで質問できていなかったら多分諦めてます。

ASのGradle Consoleには以下のようなエラーが出ていました。

Error:Cause: org/jetbrains/kotlin/lexer/KotlinLexer : Unsupported major.minor version 52.0

はい、後半部分はJavaで開発してるとわりとよく見るやつですね。Javaのバージョンが低いです。
ちなみに、build.gradle.ktsはJava 8以上が必要です。

コマンドラインからはビルドできているので ./gradlew -v しても当然Java 8だと表示されます。

------------------------------------------------------------
Gradle 4.3.1
------------------------------------------------------------

Build time:   2017-11-08 08:59:45 UTC
Revision:     e4f4804807ef7c2829da51877861ff06e07e006d

Groovy:       2.4.12
Ant:          Apache Ant(TM) version 1.9.6 compiled on June 29 2015
JVM:          1.8.0_131 (Oracle Corporation 25.131-b11)
OS:           Mac OS X 10.12.6 x86_64

ASが内部で使うJavaのバージョンが悪いと想像はつくのですがASにGradleが使うJVMの設定はありません。

スクリーンショット 2017-11-13 2.19.56.png

Gradleの中の人に助けてくれーと泣きついて出てきた解決策は「一度IntelliJ IDEAで開いて設定を変える」でした。
Ultimate版で試したので確認はしていないですがCommunity版にも多分あるんじゃないでしょうか。(雑ですいません)

スクリーンショット 2017-11-13 2.19.16.png

ここでGradle JVMの項目を1.8に変更します。その後、ASで開き直すとGradle syncできるようになります。

.ideaフォルダのファイルに差分が出ていたので .idea/gradle.xml の編集だけでも大丈夫なのではと思いますが未確認です。

Step 2 - appレベルのbuild.gradle

気合でガッと全部書き換えます(徐々に変えていく方法もあります - appendix)
ただ、appレベルを書き換える際、ルートレベルのbuild.gradleとsettings.gradleも変更する必要があります。

最初にこのstepでの最終型を示してハマリポイントを紹介していこうと思います。

build.grale.kts
allprojects {
    val kotlinVersion by extra { "1.2.0-rc-39" }
    repositories {
        google()
        jcenter()
        maven(url = "http://dl.bintray.com/kotlin/kotlin-eap-1.2")
    }
}

task("clean", Delete::class) {
    delete = setOf(rootProject.buildDir)
}
app/build.gradle.kts
import com.github.gfx.ribbonizer.FilterBuilder
import com.github.gfx.ribbonizer.GrayRibbonBuilder
import com.github.gfx.ribbonizer.YellowRibbonBuilder

plugins {
    id("com.android.application") version "3.0.0"
    kotlin("android") version "1.2.0-rc-39"
    kotlin("android.extensions") version "1.2.0-rc-39"
    id("com.github.gfx.ribbonizer") version "2.1.0"
    id("com.google.gms.google-services") version "3.1.2" apply false
}

android {
    compileSdkVersion(26)
    defaultConfig {
        applicationId = "com.chibatching.ktssample"
        minSdkVersion(17)
        targetSdkVersion(26)
        versionCode = 1
        versionName = "1.0"
        testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        getByName("debug") {
            isMinifyEnabled = false
            applicationIdSuffix = ".debug"
        }
        getByName("release") {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android.txt"), file("proguard-rules.pro"))
        }
    }
}

ribbonizer {
    withGroovyBuilder {
        "builder"(FilterBuilder { variant, iconFile ->
            return@FilterBuilder when (variant.buildType.name) {
                "debug" -> YellowRibbonBuilder().apply(variant, iconFile)
                else -> GrayRibbonBuilder().apply(variant, iconFile)
            }
        })
    }
}

val kotlinVersion: String by extra

dependencies {
    implementation(fileTree(mapOf("dir" to "libs", "include" to arrayOf("*.jar"))))
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion")

    implementation("com.android.support:appcompat-v7:26.1.0")
    implementation("com.android.support.constraint:constraint-layout:1.0.2")

    implementation("com.google.firebase:firebase-core:11.6.0")

    testImplementation("junit:junit:4.12")
    androidTestImplementation("com.android.support.test:runner:1.0.1")
    androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.1")
}

apply(mapOf("plugin" to "com.google.gms.google-services"))
settings.gradle
pluginManagement {
    repositories {
        gradlePluginPortal()
        maven { url "https://jcenter.bintray.com/" }
        maven { url "https://maven.google.com" }
        maven { url "http://dl.bintray.com/kotlin/kotlin-eap-1.2" }
    }
    resolutionStrategy {
        eachPlugin {
            switch (requested.id.id) {
                case "com.android.application":
                    useModule("com.android.tools.build:gradle:${requested.version}")
                    break
                case "com.github.gfx.ribbonizer":
                    useModule("com.github.gfx.ribbonizer:ribbonizer-plugin:${requested.version}")
                    break
                case "com.google.gms.google-services":
                    useModule("com.google.gms:google-services:${requested.version}")
                    break
            }
        }
    }
}

include ':app'

ハマリポイント 2: apply構文が(実質)使えない

appレベルのgradleの冒頭、見慣れない記述があります。

app/build.gradle.kts
plugins {
    id("com.android.application") version "3.0.0"
    kotlin("android") version "1.2.0-rc-39"
    kotlin("android.extensions") version "1.2.0-rc-39"
    id("com.github.gfx.ribbonizer") version "2.1.0"
    id("com.google.gms.google-services") version "3.1.2" apply false
}

build.gradle.ktsでは、プラグインの適用を今までのapply構文ではなくこのplugins構文で行う必要があります。

apply構文も実際には使えるのですが、その場合各プラグインのconfigurationにtype safeにアクセスができません。
つまり android { ... 等と書いてもすべてコンパイルエラーになります。

このplugins構文ではプラグインの依存解決はbuildscriptブロックではなく、settings.gradleのpluginManagementで行うことになります(上記サンプル参照)。
このpluginManagementrepositoriesブロックでgoogle()jcenter()を書いても認識されないのでmavenでurlを指定して書く必要があります。
また、このrepositoriesブロックにリポジトリURLを書くだけではplugins構文に書いたplugin idが解決できないことが多くあります。
どうやら、plugin idに指定したプラグインのartifactは[指定したid]:[指定したid].gradle.plugin:[version]の形式である必要があるらしく、この形式になっていないものはresolutionStrategyブロックで自ら解決しなくてはいけません。

resolutionStrategy {
    eachPlugin {
        switch (requested.id.id) {
            case "com.android.application":
                useModule("com.android.tools.build:gradle:${requested.version}")
                break

書き方は難しくなく、requested.id.idpluginsブロックで記載したidが入っているので、その値を見て元々buildscriptブロックで指定していたclasspathの値で解決するように指定するだけです。

ハマリポイント 3: 任意の箇所でapplyしたい

firebaseを使うときにはcom.google.gms.google-servicesプラグインをbuild.gradleの末尾でapplyする必要があります。
しかし、plugins構文を使うと即時applyされる && pluginsブロックはファイル内に1つという制限があります。

applyのタイミングを任意に変える時はpluginのid指定時にapply falseを指定することで即時applyを防ぎ、適用したい箇所で改めてapplyを記述することで実現できます。

plugins {

    ...

    id("com.google.gms.google-services") version "3.1.2" apply false
}

...

apply(mapOf("plugin" to "com.google.gms.google-services"))

長くなってしまったのでpart 2に続きます(予定)。

appendix

apply {
  from("${project.rootDir}/app/old_build.gradle")
}

という感じで以前のbuild.gradleを読み込んで徐々にkts化していくことができそうです

続きはこちら
build.gradle.ktsへの書き換えとハマリポイント part 2