# AndroidプロジェクトのGradleの設定など

AndroidのGradleプロジェクトを作成する際に毎回やっていることなどをメモしたいと思います。

びぇびぇ

サブネミミッミ「GradleをKotlin DSL化するにぇ。」

# 1. Gradle Wrapperのバージョンの更新

gradle/wrapper/gradle-wrapper.properties はこんな感じになっています。

Gradle Distributions を見ながら最新のstableリリースを指定します。
2020/05/02現在では 6.3 が最新のstable版でした。

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip

# 2. .editorconfigの作成

プロジェクトのルートに .editorconfig という名前で以下の内容を書き込みます。
後述する ktlint でもここでの設定を利用してくれるみたいです。

root=true

[*.{kt,kts,java,xml}]
charset=utf-8
indent_style=space
indent_size=4
trim_trailing_whitespace=true
insert_final_newline=true
max_line_length=120
end_of_line=lf

# 3. gradle.propertiesの設定

プロジェクトルートの gradle.properties を以下のように書き換えます。
これよってAndroidXやincrementalビルドを有効にできます。

kotlin.code.style=official
kapt.incremental.apt=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
android.databinding.incremental=true
android.useAndroidX=true

# 4. buildSrcの設定

マルチモジュールのプロジェクトにすることが多いので、ライブラリのバージョンを一箇所にまとめて記述します。

# build.gradle.ktsの設定

プロジェクトのルートに buildSrc/ というディレクトリを作ります。
そして直下に build.gradle.kts を作成して以下の内容を書き込みます。

plugins { `kotlin-dsl` }

repositories {
    jcenter()
    google()
}

dependencies {
    implementation("com.android.tools.build:gradle:3.6.3")
}

"com.android.tools.build:gradle:3.6.3" というのはAndroidプロジェクト用のGradleプラグインです。
バージョンは使用しているAndroid Studioのバージョンと同じものを指定しましょう。
Google's Maven Repositorycom.android.tools.build/gradle/... でもバージョンを確認することができます。

  .
  .
  .
+ com.android.tools.analytics-library
+ com.android.tools.apkparser
- com.android.tools.build
    aapt2
    aapt2-proto
    aaptcompiler
    apksig
    apkzlib
    builder
    builder-model
    builder-test-api
    bundletool
    gradle
        3.0.0-alpha1
        3.0.0-alpha2
        3.0.0-alpha3
        .
        .
        .
        3.6.0
        3.6.1
        3.6.2
        3.6.3  ← 私の環境ではコレ
        4.0.0-alpha01
        4.0.0-alpha02
        4.0.0-alpha03
        .
        .
        .

また、自動では追加されないので .gitignore も作成しておきましょう。

build/

ここで一旦Gradleのsyncを掛けましょう。
🐘のアイコンをクリックすることで掛かります。

# アプリのメタ情報の記述

次に buildSrc/src/main/kotlin/ というようなディレクトリを作り、その中に Project.kt を作成します。
そこにAndroidアプリのパッケージ名やバージョンなどを以下のように記述します。

object Project {
    const val applicationId = "net.aridai.myapp"
    const val targetSdk = 29
    const val minSdk = 26
    const val compileSdk = 29
    const val versionCode = 1
    const val versionName = "1.0.0"
    const val sdkBuildTools = "29.0.2"
    const val kotlinVersion = "1.3.61"
}

sdkBuildToolsSDK Build Tools release notes | Android Developers で最新バージョンを確認することができます。
しかし、リリース直後だとGitHub ActionsでSDK Build Toolsが見つからないというエラーが出てコケたことがあったので、上記では最新の 29.0.3 ではなく 29.0.2 を使っています。

kotlinVersion はKotlinの言語のバージョンです。
Maven Central で最新のバージョンを確認することができます。

# 使用ライブラリの記述

buildSrc/src/main/kotlin/Dependencies.kt を作り、使用ライブラリについて以下のように記述します。
(あくまで一例ですので、必要に応じて追加していってください。)

object Dependencies {

    object Kotlin {
        //  https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib-jdk8
        const val stdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Project.kotlinVersion}"

        //  https://mvnrepository.com/artifact/org.jetbrains.kotlinx
        object Coroutines {
            private const val version = "1.3.3"

            //  https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
            const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"

            //  https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-android
            const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version"

            //  https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-rx2
            const val rx2 = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$version"

            // https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-test
            const val test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$version"
        }
    }

    object AndroidX {

        //  https://mvnrepository.com/artifact/androidx.appcompat/appcompat
        const val appcompat = "androidx.appcompat:appcompat:1.2.0-alpha02"

        //  https://mvnrepository.com/artifact/androidx.constraintlayout/constraintlayout
        const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.0.0-beta4"

        //  https://mvnrepository.com/artifact/androidx.core/core-ktx
        const val coreKtx = "androidx.core:core-ktx:1.2.0"

        //  https://mvnrepository.com/artifact/androidx.fragment/fragment-ktx
        const val fragmentKtx = "androidx.fragment:fragment-ktx:1.2.2"

        //  https://mvnrepository.com/artifact/androidx.arch.core
        object ArchCore {
            private const val version = "2.1.0"

            // https://mvnrepository.com/artifact/androidx.arch.core/core-testing
            const val coreTesting = "androidx.arch.core:core-testing:$version"
        }

        //  https://mvnrepository.com/artifact/androidx.lifecycle
        object Lifecycle {
            private const val version = "2.2.0"

            //  https://mvnrepository.com/artifact/androidx.lifecycle/lifecycle-extensions
            const val extensions = "androidx.lifecycle:lifecycle-extensions:$version"

            //  https://mvnrepository.com/artifact/androidx.lifecycle/lifecycle-livedata-ktx
            const val liveDataKtx = "androidx.lifecycle:lifecycle-livedata-ktx:$version"

            //  https://mvnrepository.com/artifact/androidx.lifecycle/lifecycle-viewmodel-ktx
            const val viewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$version"
        }

        //  https://mvnrepository.com/artifact/androidx.test
        object Test {
            private const val version = "1.2.0"

            //  https://mvnrepository.com/artifact/androidx.test/core-ktx
            const val coreKtx = "androidx.test:core-ktx:$version"
        }
    }

    //  https://mvnrepository.com/artifact/org.koin
    object Koin {
        private const val version = "2.1.1"

        //  https://mvnrepository.com/artifact/org.koin/koin-core-ext
        const val coreExt = "org.koin:koin-core-ext:$version"

        //  https://mvnrepository.com/artifact/org.koin/koin-androidx-ext
        const val androidXExt = "org.koin:koin-androidx-ext:$version"

        //  https://mvnrepository.com/artifact/org.koin/koin-androidx-viewmodel
        const val viewModel = "org.koin:koin-androidx-viewmodel:$version"

        // https://mvnrepository.com/artifact/org.koin/koin-test
        const val test = "org.koin:koin-test:$version"
    }

    object Robolectric {
        private const val version = "4.3.1"

        // https://mvnrepository.com/artifact/org.robolectric/robolectric
        const val robolectric = "org.robolectric:robolectric:$version"
    }

    //  https://mvnrepository.com/artifact/io.reactivex.rxjava2
    object RxJava2 {

        // https://mvnrepository.com/artifact/io.reactivex.rxjava2/rxjava
        const val rxJava = "io.reactivex.rxjava2:rxjava:2.2.19"

        // https://mvnrepository.com/artifact/io.reactivex.rxjava2/rxandroid
        const val rxAndroid = "io.reactivex.rxjava2:rxandroid:2.1.1"

        // https://mvnrepository.com/artifact/io.reactivex.rxjava2/rxkotlin
        const val rxKotlin = "io.reactivex.rxjava2:rxkotlin:2.4.0"
    }

    // https://mvnrepository.com/artifact/com.github.bumptech.glide/glide
    const val glide = "com.github.bumptech.glide:glide:4.11.0"

    // https://mvnrepository.com/artifact/com.google.android.material/material
    const val material = "com.google.android.material:material:1.1.0"

    //  https://mvnrepository.com/artifact/junit/junit
    const val junit = "junit:junit:4.13"

    // https://mvnrepository.com/artifact/io.mockk/mockk
    const val mockK = "io.mockk:mockk:1.9.3"
}

このように記述することによって、各モジュールの build.gradle.kts で以下のようにライブラリを指定することができるようになります。

dependencies {
    implementation(fileTree("dir" to "libs", "include" to listOf("*.jar")))

    implementation(Dependencies.Kotlin.stdLib)
    implementation(Dependencies.Kotlin.Coroutines.core)
    implementation(Dependencies.Kotlin.Coroutines.android)

    implementation(Dependencies.AndroidX.appcompat)
    implementation(Dependencies.AndroidX.constraintLayout)
    implementation(Dependencies.AndroidX.coreKtx)
    implementation(Dependencies.AndroidX.fragmentKtx)
    .
    .
    .
}

# build.gradle.kts用の拡張

次に buildSrc/src/main/kotlin/Extensions.kt を作り、以下を記述します。
これによって、Kotlin DSLでのGradleの設定が書きやすくなります。

import com.android.build.gradle.BaseExtension
import com.android.build.gradle.api.AndroidSourceSet
import com.android.build.gradle.internal.dsl.BuildType
import org.gradle.api.NamedDomainObjectContainer

fun org.gradle.api.Project.android(action: BaseExtension.() -> Unit) {
    val extension = extensions.getByName("android") as? BaseExtension
    if (extension != null) action.invoke(extension)
}

fun NamedDomainObjectContainer<BuildType>.release(body: BuildType.() -> Unit) {
    getByName("release") { body(this) }
}

val NamedDomainObjectContainer<AndroidSourceSet>.main: AndroidSourceSet get() = getByName("main")

val NamedDomainObjectContainer<AndroidSourceSet>.test: AndroidSourceSet get() = getByName("test")

val NamedDomainObjectContainer<AndroidSourceSet>.androidTest: AndroidSourceSet get() = getByName("androidTest")

参考: build.gradle.ktsのandroidブロックをbuildSrcで共通化させてみる - pixiv inside by @m4kvn

# 5. GradleをKotlin DSLで書き換え

既存の build.gradlebuild.gradle.kts に書き換えていく作業になります。

# settings.gradleをKotlin DSL化

プロジェクトルートの settings.gradle を以下のように書き換えて settings.gradle.kts にリネームします。 (初期状態の場合)

rootProject.name = "MyApp"
include(":app")

もし、マルチモジュールの場合は以下のような感じになるかと思われます。

val modulesDir = File("modules")
val myFeatureDir = File(modulesDir, "myfeature")

include(":app")

include(":common")
project(":common").projectDir = File(modulesDir, "common")

include(":myfeature-application", ":myfeature-data", ":myfeature-domain", ":myfeature-ui")
project(":myfeature-application").projectDir = File(myFeatureDir, "myfeature-application")
project(":myfeature-data").projectDir = File(myFeatureDir, "myfeature-data")
project(":myfeature-domain").projectDir = File(myFeatureDir, "myfeature-domain")
project(":myfeature-ui").projectDir = File(myFeatureDir, "myfeature-ui")

rootProject.name = "MyApp"

これはこのようなモジュール構成にした場合の例です。

(プロジェクトルート)
├── app
├── buidSrc
├── modules
    ├── common
    └── myfeature
        ├── myfeature-application
        ├── myfeature-data
        ├── myfeature-domain
        └── myfeature-ui

app というメインのモジュールがあり、modules/ ディレクトリ下に各モジュールが配置されています。
common というモジュールが直下にあり、myfeature というディレクトリ化に myfeature-* という4つのモジュールが置かれています。

(私は技術的なパッケージングとしてこのような切り方をよくやっています。)

# ルートのbuild.gradleのKotlin DSL化

プロジェクトルートの build.gradle を以下のように書き換えて build.gradle.kts にリネームします。

この設定では各モジュールで同じような設定を一箇所にまとめて記述しています。
また、LintやData Bindingの設定もしています。

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType

plugins {
    id("org.jlleitschuh.gradle.ktlint") version "9.2.1"
    id("com.github.ben-manes.versions") version "0.28.0"
}

buildscript {
    repositories {
        jcenter()
        mavenCentral()
        google()
    }

    dependencies {
        classpath("com.android.tools.build:gradle:3.6.3")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Project.kotlinVersion}")
    }
}

allprojects {
    repositories {
        jcenter()
        mavenCentral()
        google()
    }

    apply(plugin = "org.jlleitschuh.gradle.ktlint")

    ktlint {
        android.set(true)
        outputColorName.set("RED")
        ignoreFailures.set(true)
        disabledRules.set(listOf("import-ordering", "no-wildcard-imports"))
        reporters { reporter(ReporterType.CHECKSTYLE) }
    }
}

subprojects {
    apply(plugin = if (name == "app") "com.android.application" else "com.android.library")
    apply(plugin = "org.jetbrains.kotlin.android")
    apply(plugin = "org.jetbrains.kotlin.android.extensions")

    android {
        compileSdkVersion(Project.compileSdk)
        buildToolsVersion = Project.sdkBuildTools

        defaultConfig {
            if (name == "app") applicationId = Project.applicationId
            minSdkVersion(Project.minSdk)
            targetSdkVersion(Project.targetSdk)
            versionCode = Project.versionCode
            versionName = Project.versionName
            testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        }

        buildTypes {
            release {
                isMinifyEnabled = false
                proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
            }
        }

        lintOptions {
            xmlReport = true
            isAbortOnError = false
            disable = setOf()
        }

        testOptions.unitTests.isIncludeAndroidResources = true
        dataBinding.isEnabled = true

        sourceSets {
            main.java.srcDirs("src/main/kotlin")
            test.java.srcDirs("src/test/kotlin")
            androidTest.java.srcDirs("src/androidTest/kotlin")
        }

        compileOptions {
            sourceCompatibility = JavaVersion.VERSION_1_8
            targetCompatibility = JavaVersion.VERSION_1_8
        }
    }

    tasks.withType<KotlinCompile> {
        sourceCompatibility = JavaVersion.VERSION_1_8.toString()
        targetCompatibility = JavaVersion.VERSION_1_8.toString()
        kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
    }
}

tasks.register<Delete>("clean") {
    delete(rootProject.buildDir)
}

なお、この設定では以下のプラグインを導入しています。

# 各モジュールのbuild.gradleのKotlin DSL化

各モジュールの build.gradle には主に dependencies { } を書くようにします。

非同期処理にはKotlin CoroutinesとRxJavaの両方を使っていて、AACのLiveDataとViewModelも使っていて、DIコンテナにKoinを使っていて、テストはRobolectricを使っている人 (aridai) の場合はこんな感じになります。

dependencies {
    implementation(fileTree("dir" to "libs", "include" to listOf("*.jar")))

    implementation(Dependencies.Kotlin.stdLib)
    implementation(Dependencies.Kotlin.Coroutines.core)
    implementation(Dependencies.Kotlin.Coroutines.android)
    implementation(Dependencies.Kotlin.Coroutines.rx2)

    implementation(Dependencies.AndroidX.appcompat)
    implementation(Dependencies.AndroidX.constraintLayout)
    implementation(Dependencies.AndroidX.coreKtx)
    implementation(Dependencies.AndroidX.fragmentKtx)
    implementation(Dependencies.AndroidX.Lifecycle.extensions)
    implementation(Dependencies.AndroidX.Lifecycle.liveDataKtx)
    implementation(Dependencies.AndroidX.Lifecycle.viewModelKtx)
    implementation(Dependencies.Koin.coreExt)
    implementation(Dependencies.Koin.androidXExt)
    implementation(Dependencies.Koin.viewModel)
    implementation(Dependencies.RxJava2.rxJava)
    implementation(Dependencies.RxJava2.rxAndroid)
    implementation(Dependencies.RxJava2.rxKotlin)
    implementation(Dependencies.material)

    testImplementation(Dependencies.junit)
    testImplementation(Dependencies.mockK)
    testImplementation(Dependencies.Kotlin.Coroutines.test)
    testImplementation(Dependencies.AndroidX.ArchCore.coreTesting)
    testImplementation(Dependencies.AndroidX.Test.coreKtx)
    testImplementation(Dependencies.Koin.test)
    testImplementation(Dependencies.Robolectric.robolectric)
}

ここまで書けたらGradleのsyncを掛けましょう。

# その他の設定

sourceSetの設定をしてあるので、ソースファイルの置き場所を src/main/kotlin にすることができます。
また、単体テストの雛形を以下のように作っておきます。

package net.aridai.myapp

import android.os.Build
import io.mockk.clearAllMocks
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.context.stopKoin
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@ExperimentalCoroutinesApi
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.P])
class ExampleUnitTest {

    private lateinit var testCoroutineDispatcher: TestCoroutineDispatcher

    @Before
    fun setup() {
        testCoroutineDispatcher = TestCoroutineDispatcher()
        Dispatchers.setMain(testCoroutineDispatcher)
    }

    @After
    fun teardown() {
        Dispatchers.resetMain()
        clearAllMocks(answers = false)
        stopKoin()
    }

    @Test
    fun てすと1() = Unit

    @Test
    fun てすと2() = Unit

    @Test
    fun てすと3() = Unit
}

これによって Dispatchers.Main を使用しているコード (ViewModel#viewModelScopelaunch 等) でもちゃんとテストできます。

# 6. Dangerの設定

次はCIのための設定です。
DangerというLintの指摘結果をRPのレビューで怒ってくれるツールを導入します。

プロジェクトのルートに Gemfile というファイルを作り、以下を書き込みます。

source "https://rubygems.org"

gem "danger"
gem "danger-android_lint"
gem "danger-checkstyle_format"

そして Dangerfile というファイルを作り、以下を書き込みます。

android_lint.skip_gradle_task = true
Dir.glob("**/reports/lint-results.xml").each { |report|
  android_lint.report_file = report.to_s
  android_lint.lint(inline_mode: true)
}

checkstyle_format.base_path = Dir.pwd
Dir.glob("**/ktlint/*.xml").each { |report|
  checkstyle_format.report report.to_s
}

DangerはRuby製のツールですが、CIでのみ使う場合はローカルマシンに環境を構築しなくても大丈夫です。

# 7. GitHub Actionsの設定

次にCIの設定です。
GitHubをブラウザで開いてそこで作業します。

Actions > New workflow > set up a workflow yourself から直接ymlを編集できる画面に行きます。
そこで以下の内容をコピペします。

name: CI

on:
  pull_request:
    branches: master
  push:
    branches: master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: actions/cache@v1
        with:
          path: ~/.gradle/caches
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Build
        run: ./gradlew assemble

      - name: Test
        run: ./gradlew test

      - name: Lint
        run: ./gradlew lint ktlintCheck

      - name: Setup Ruby
        uses: actions/setup-ruby@v1
        with:
          ruby-version: '2.6'
          architecture: 'x64'

      - name: Run Danger
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gem install bundler
          bundle install
          danger

やっていることは以下の通りです。

  • Gitのcheckout
  • キャッシュ (*.gradle.ktsからハッシュ値を計算)
  • ビルドの実行
  • テストの実行
  • Lintの実行
  • Rubyの設定
  • Dangerの実行

にぇにぇ

サブネミミッミ「これで快適なAndroidアプリ開発ができるにぇ。」