はじめに
この記事は「Kotlin 1.1以降をAndroidで安全に使いたかった話」です。本当は会社のAdvent Calendar向けに書くはずだったのですが、色々あってお蔵入りになったのでこちらで供養。 ちなみにこれはボツネタ第2弾で、第1弾はこちら。 muumuutech.hatenablog.com
Kotlin 1.1 とJava 8
Kotlin 1.1がリリースされて久しいが、1.1.系からJava 8 に依存したAPIがいくつか追加された。AndroidはAPI 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-api
とlint-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を作ることはできた。
感想
Custom ListをよりによってKotlinで作ろうとして一番大変だったのは、とにかくドキュメントがないこと。公式もそうだが、全体的に情報が少ない。ヒットしても数年前のものだったりして現状で動かなくなったりして困った。無駄にCustom Listの中のコードを読んでちょっと詳しくなってしまった気がする…
もしもどこかに体系化された素晴らしいドキュメントがあれば教えてください(´;ω;`)
Links
実装にあたりこれらの記事を参考にした。
Help developers with custom Lint rules · Jeremie Martinez
www.slideshare.net
-
Jet Brains 公式ドキュメントでは
kotlin-stdlib-jre7
とkotlin-stdlib-jre8
を使えと書いている。kotlin-stdlib-jre7
だけを指定するとJava 8 に依存した機能を使った時にコンパイルエラー出してくれるかな?と期待したが、残念ながらエラーは出なかった。kotlin-stdlib-jre8
を指定しようと思ったらAndroidではJack and Jill
へ依存を持っているようでgradleのsyncすらできなかった。(Jack and Jill
はdeprecatedになりましたね…)↩ -
むしろデフォルトでチェックがついているJava 8 migration aidsの
Replace with single Map method
でMap.getOrDefault()を使うように推奨してきます😇↩