KotlinでAndroidライブラリを作ってリリースする


はじめに

KotlinでAndroid-PageControlという簡単なライブラリを作ってみました。
ViewPagerIndicatorなど著名なライブラリが多数あり散々使われているパターンではありますが、iOSのPageControlのようにインジケータをタップしてもページが変わる、というのを実現するために作ってみました。
小さなライブラリなので、他の言語で書いてみようと思い、今回はKotlinでの開発にチャレンジしてみました。
以下、導入からMaven Centralリリースに至るまでのメモです。

※前提として、Kotlinを使いこなしている人でなく、「AndroidアプリをJava以外でもっと簡単に書けないか?」と探っている、Kotlin初心者による初心者向けの内容です。
※XXX言語でも書ける、XXXは使うべきじゃない、等々あると思いますが、あくまでこのケースにおける比較ということで見ていただければと思います。

開発環境への導入

Android StudioにIntelliJ IDEAのKotlinのプラグインを導入します。
また、Gradleではkotlin-gradle-pluginとkotlin-stdlibを導入します。
(詳細は割愛)

導入で注意するところとしては、バージョンの選択でしょうか。
Maven Centralでは、現時点でkotlin-gradle-plugin/kotlin-stdlibの0.8.679がリリースされていますが、これを使うとIntelliJのプラグインのバージョン(0.8.11)と合っていないためかエラーが発生しSyncできません。
そのため0.8.11を使っています。
build.gradleは以下のようになりました。(Kotlin Pluginがサポートしてくれた部分もあります)

build.gradle
buildscript {
    ext.kotlin_version = '0.8.11'
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.12.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
:

Javaからの移行

上記のライブラリははじめにJavaで書いていたので、
1つずつ書き換えないといけないのか…と思っていましたがもっと簡単でした。
Javaファイルのコードをkotlinディレクトリ上のKotlinソースファイル(.kt)にコピー&ペーストするとConvertしてくれました。
エラーも多数出ており完璧ではありませんでしたが、数クラス程度なら大分楽になると思います。

コーディング

コーディング上の注意点等は書く内容によると思いますので、今回のケースでJavaとの違いとして目立ったところ、Kotlinへの変換時に自動でなく手で書き直した部分などをいくつかメモしておきます。

コンストラクタは1つ

今回のようなカスタムViewを作る場合、コンストラクタを複数用意する(パラメータのContext, AttributeSet, intでの組み合わせ)と思いますが、コンストラクタは1つしか定義できないため、今回はXMLでの利用を想定してContextAttributeSetを引数に持つコンストラクタを定義しました。

open class PageControl(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs, 0) {

nullでない場合にのみ実行する

KotlinではNPEを防ぐための記述がいろいろと用意されています。
たとえば以下のような記述でエラーを出します。

// getTheme()はnullを返す可能性があるためエラーが出る
context.getTheme().obtainStyledAttributes(attrs, R.styleable.AndroidPageControl, R.attr.apcStyles, 0)

この場合にgetTheme()の後に!!を入れる、もしくは?を入れることでエラーが解消しますが、どちらを使うかで(当然ですが)意味が変わってきます。
!!の場合、「nullだった場合にはNullPointerExceptionをスローする」となります。
一方?を使えば、「nullでない場合にのみ実行する]
ことができるようです。
したがって、nullチェックをしてからアクセスしたいようなケースでは?を使った方が良いはずです。

context.getTheme()?.obtainStyledAttributes(attrs, R.styleable.AndroidPageControl, R.attr.apcStyles, 0)

Overrideはnullを許容するかを区別する

Activity#onCreate(Bundle)など、APIをオーバーライドするものが多いと思いますが、このときのパラメータの型に?を付けるかどうかでシグネチャが変わるためオーバーライドしているつもりがエラーとなったりします。
基本的にはJavaのAPIをオーバーライドしたらnullが入ってくる可能性があると思いますのでv: View?というようにnull許可になるのではないかと思います。

    // OK
    protected override fun onCreate(savedInstanceState: Bundle?) {
    // NG: 'onCreate' overrides nothing
    protected override fun onCreate(savedInstanceState: Bundle) {

nullの場合の値を指定する

メソッドの戻り値を使ってさらにメソッドを呼び出し、という連鎖の結果を使う場合、その連鎖の途中で戻り値がnullになるケースを考慮する必要があると思いますが、Javaでそれを書こうとするとなかなか長い記述になることもあると思います。
Kotlinでは以下のように?:(エルビス演算子)を使うことでnullの場合の値を指定できます。

mNumOfViews = mViewPager?.getAdapter()?.getCount() ?: 0

これはGroovyでも(Java 8でもでしょうか?)できますね。

三項演算子でなくif-else

Kotlinでは式としてif / elseを使えるようです。

setColor(isCurrent ? mColorCurrentDefault : mColorNormalDefault);
setColor(if (isCurrent) mColorCurrentDefault else mColorNormalDefault)

TargetApiアノテーションをつけても意味がない

あるAPIレベル以上でのみ利用できるAPIを使う場合に、Java(Android)では以下のように@TargetApiアノテーションを使うと思いますが、これがKotlinでは(つけられるものの)付けてもあまり意味がないようです。
Deprecatedの警告が残ります。
(Kotlinというよりはプラグイン等の問題でしょうけれど)

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public void setBackground(....) {
    :
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
        b.setBackgroundDrawable(sld)
    } else {
        b.setBackground(sld)
    }
}

Android Studio(Kotlin Plugin)の警告とGradleの結果が違う?

これは意味が分からず大分時間をかけてしまいましたが、
IDE(Kotlin Plugin)上の警告とGradleの警告が相反した内容になっているケースがありました。
Mapを要素に持つListを、Mapのある要素でソートする、というJavaの処理をCollatorとComparatorでやっていましたが、この移行がうまくいきませんでした。
(今はコンパイルエラーにはなっていませんが結局うまく動いていないです)
現状のコードが下記です。

    data.sortBy(object : Comparator<Map<String, Any>> {
        override fun compare(lhs: Map<String, Any>, rhs: Map<String, Any>): Int {
            return Collator.getInstance()!!.compare(lhs.get("className") as String, rhs.get("className") as String)
        }
    })

IDE(Kotlin Plugin)の出す警告は「Comparatorのメソッドをimplementしていない。Map<String, Any>?をパラメータに持つメソッドをオーバーライドしろ」というものですが、これに従いMap<String, Any>Map<String, Any>?にするとGradleでビルド失敗し、逆のことを言われます...
最終的にKotlin Pluginの警告(といっても赤いエラー表示なので非常に気になるのですが)は無視してもビルドできるのでGradleの結果優先で上記のようにしています。
(動いていないのはサンプルアプリ内のコードで、ライブラリ自体はきちんと動きます ^^;)

ライブラリの利用

ライブラリプロジェクトにサンプルアプリを同梱していますが、
利用する側は普通のライブラリとして利用できます。
今回はサンプルアプリも一部Kotlinで書いていることや、ライブラリをサブプロジェクトとして取り込んでいることもあってbuild.gradleの書き方は通常と違っていますが、通常は以下のようにdependenciesに追記するだけです。

dependencies {
    compile 'com.android.support:support-v4:20.0.0'
    compile 'com.github.ksoichiro:androidpagecontrol:0.1.0'
}

Travis CIでのビルド

ビルドがGradleだけでできるので、特に注意すべき点はありませんでした。
普通に.travis.ymlをAndroid用に書けばビルドできます。
今回はサンプルアプリでの動作確認のみでテストコードは書いていませんので、自動テストにおける注意点があるかは分かりません。

Maven Centralへのリリース

AndroidライブラリをMaven Centralにリリースするために、Javadocのjarファイルもリリースする必要がありますが、KotlinではJavadocではなくKDocというのを使うようです。
このため通常のJavadocタスクが実行できません。

Chris Banesさんのgradle-mvn-pushを使おうと思いましたがJavadocタスクが失敗し、うまく動きませんでした。
Kotlinの他のライブラリはどうなっているの?といくつかJetBrains社の出しているKotlinのライブラリのJavadocをダウンロードしてみると、中身がREADMEしかありませんでした...
まだ対応していないということのようです。
kotlin-gradle-plugin-coreにはKDocというTaskはあるのですが、これがpluginとしてはAndroidプラグインに依存した状態でしか提供されておらず、gradle-mvn-pushの中に含めて利用する(maven, signingプラグインと共存する)ことができませんでした。
というわけで、残念ながら今回はgradle-mvn-pushをベースに、Javadoc生成を迂回する方法(READMEだけのjar)をとりました。

感想

ありふれた内容ではありますが、今回の作業を通じての所感です。
全体としては良い(書きやすそうな)印象を持ちました。
継続してチェックしていこうと思います。

良さそうな点

  • 普段Android Studioを使う人なら導入はすごく簡単
  • Kotlin Pluginが自動で変換してくれる量が多く手軽だった
  • JavaとKotlinのコードがプロジェクト内で共存できるので段階的な移行がしやすそう
  • Null-safetyに関する仕組みが多く、NPEに繋がる記述をエラーとしてくれるので安心して書きやすい
  • 全体として(多少)記述量が減らせた

今ひとつ...な点

  • IDEとGradleの結果が異なっているケースがあるのは不安
  • まだWeb上に情報が少なく、英語で見つけても言語仕様的に古かったりするのでハマると困る...
  • 言語そのものは良さそうだが、関連ツール群を含めるとJavaとのギャップが大きいかも(MavenやGradle)

以上です。