言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

Kotlin 1.1以降をAndroidで安全に使いたかった話

はじめに

この記事は「Kotlin 1.1以降をAndroidで安全に使いたかった話」です。本当は会社のAdvent Calendar向けに書くはずだったのですが、色々あってお蔵入りになったのでこちらで供養。 ちなみにこれはボツネタ第2弾で、第1弾はこちら。 muumuutech.hatenablog.com

Kotlin 1.1 とJava 8

Kotlin 1.1がリリースされて久しいが、1.1.系からJava 8 に依存したAPIがいくつか追加された。AndroidAPI Level 24からJava 8 に対応しているので、23以下の端末でうっかり該当APIが実行されるとCrashしてしまう

Java 8 に依存したAPI@PlatformDependentアノテーションが付与されている。今の所このアノテーションが付与されているのは、MapインターフェイスgetOrDefault()MutableMapインターフェイスremove()の二つ。

これらのAPIを安全に扱うためにCustom Lintの導入を試みた。1

Custom Lintを作ろう

Android LintもしくはJava Lintに使えそうなものがないか確認したところ2なさそうなので作っていく。gradleの設定をして、Detector, Issue, Registryの順に作っていく。

最初にGradleでビルドするプロジェクトを作る。言語は今回Kotlinにした。

Gradle

プロジェクトができたらGradleを書いて行く。 lint-apilint-checksを追加する。(kotlinを使う場合はkotlin用の依存も追加)

dependencies {
    def lintVersion = "25.3.0"
    compile "com.android.tools.lint:lint-api:$lintVersion"
    compile "com.android.tools.lint:lint-checks:$lintVersion"

}

そしてここがポイントなのだが、Lint-Registry-v2 でRegistoryを定義する。色々ググったらLint-Registryで定義している情報が出てくるが、2017年末時点ではLint-Registry-v2で定義しないとLintがうまく動かなかった。(これに気がつかず何時間も無駄にしてしまった…)

jar {
    manifest {
        attributes("Lint-Registry-v2": "com.your.package.CustomLintRegistry")
    }
}

Detector

今回やりたいことは「特定のアノテーション(@PlatformDependent)が付与されたAPIを使用した際にwarningをだす」である。本来であればJavaPsiScannerを使ってASTに解析されたノードを探索して行って…みたいなことをするべきなんだろうけど、Advent Calendar向けにはヘビィだったので妥協案でいくことにする。

該当APIが付与されたAPIはたったの2個なので、「それらが使われた時にwarningをだす」でとりあえずお茶を濁すことにする。特定のコードが記述されているか検知したいので(まずはよく使うgetOrDefault()でlintチェックが出るか試す)、 ClassScanner インターフェイスを使い、必要なメソッドをoverrideしていく。

getApplicableCallNames()

detectorが検知したいメソッド名のリストを返すメソッド。今回は特定のメソッドがコールされたことを検知したいので、そのメソッド名をstringのmutable listにして返してやる。

    override fun getApplicableCallNames(): MutableList<String> = mutableListOf("getOrDefault")

checkCall()

detectorが上記getApplicableCallNames()で定義したメソッドを検知すると呼ばれるメソッド。本当はpackageのチェックとかやらなきゃいけないがとりあえずノーチェックで通す。

    override fun checkCall(context: ClassContext?, classNode: ClassNode?, method: MethodNode?, call: MethodInsnNode?) {
            context?.report(ISSUE, method, call, context.getLocation(call),
                    "Don't use Java 8 dependency API")
    }

Issue

DetectorクラスのstaticなオブジェクトとしてIssue.create() メソッドを使って作成する。 適当に各パラメータを設定しよう。Implementation の第1引数には上記で作成したDetectorのクラスオブジェクトを渡す。

class CustomLintDetector : Detector(), Detector.ClassScanner {

    companion object {
        val ISSUE: Issue = Issue.create(
                "Java8Api",
                "Java 8 API is used",
                "Java 8 API is used. This causes crash with Android OS level under 23.",
                Category.CORRECTNESS, 6, Severity.WARNING,
                Implementation(CustomLintDetector::class.java, Scope.CLASS_FILE_SCOPE))
    }

Custom Lintを実行する

ここまでで作ったプロジェクトをbuildして作成されたjarファイルを.android/lint/配下に格納し、./gradlew lint コマンドを叩けばlintが動く。

ここからが本当にわけがわからないのだが、fat jarにしないとうまく動いたり動かなかったりする。(自分で書いていて「そんなことある?」って今思ってます…)

jarファイルを作る環境のJavaのバージョンだったり、gradleのバージョンだったりをいくつか試してみたが、どういう時に動いてどういう時に動かなかったか全然切り分けができなかった。Gradleのcacheを疑ったがこれもダメ。

そう行ったわけでQiitaで書いているAdvent Calendarに載せるわけには行かなかったのでこちらで供養。なんだかモヤモヤしたままだが、とりあえずfat jarにしておけばこんな感じでCustom Lintを作ることはできた。f:id:muumuumuumuu:20180106162609p:plain

感想

Custom ListをよりによってKotlinで作ろうとして一番大変だったのは、とにかくドキュメントがないこと。公式もそうだが、全体的に情報が少ない。ヒットしても数年前のものだったりして現状で動かなくなったりして困った。無駄にCustom Listの中のコードを読んでちょっと詳しくなってしまった気がする… もしもどこかに体系化された素晴らしいドキュメントがあれば教えてください(´;ω;`)

Links

実装にあたりこれらの記事を参考にした。


qiita.com


tools.android.com


Help developers with custom Lint rules · Jeremie Martinez


www.slideshare.net


www.bignerdranch.com


qiita.com


  1. Jet Brains 公式ドキュメントではkotlin-stdlib-jre7kotlin-stdlib-jre8を使えと書いている。kotlin-stdlib-jre7だけを指定するとJava 8 に依存した機能を使った時にコンパイルエラー出してくれるかな?と期待したが、残念ながらエラーは出なかった。kotlin-stdlib-jre8を指定しようと思ったらAndroidではJack and Jillへ依存を持っているようでgradleのsyncすらできなかった。(Jack and Jillはdeprecatedになりましたね…)

  2. むしろデフォルトでチェックがついているJava 8 migration aidsの Replace with single Map methodでMap.getOrDefault()を使うように推奨してきます😇