KotlinをAndroid Studioで触って一か月間経った話


ディップ Advent Calendarの13日目です。

Kotlinを触り始めた理由

私個人はAndroidアプリの実装はJavaで書き慣れているので、今のところKotlinに対する需要はあんまりない。
だが、導入を検討しないで放置できるほどでもないほど、Kotlinが盛り上がりを見せていると思っている。
また、Swiftエンジニアが楽ちんというからにはKotlin触っていればSwiftにも慣れやすくなるのではという淡い期待もあって触ってみることにした。
まだまだ触って一ヶ月程度なので、あまり知見が溜まっているとは言い難いが、それなりにハマるところにはハマった感があるので、ここで共有してきたい。

ここから先の話

ここから先はこれからKotlinを触ろうとしている人に向けて、私がハマった落とし穴に共有することで同じ穴にはまらないようにするために書きます。
すでにKotlinを触っている方にはあんまり珍しい話はではないと思いますので、軽く読み流していただければと思います。

Kotlinは0.x版と1.0版では細かいところで書き方が異なる。

Kotlinは2011年7月にJetBeans社がドラフトバージョンを発表してから、安定稼働版の1.0版が2016年2月にリリースされるまでの間に実に4年半近くかかっています。
その4年半の間に0.x版公開当時のノウハウがインターネット上に広く公開されているため、調べ物をする時によく見ないと混乱することになります。

私は有志の方が解説しているこちらのサイトを読みながら、Webブラウザ上でKotlinプログラムを実行し、練習できるTry Kotlinで書いたプログラムを動作させて最初は勉強してましたが、解説サイトは0.x版での解説であるため、「あれ、コンパイル通らないぞ?」のような事態に陥ることがあります。

例えば、こちらのコード。

isUpperCase
fun isUpperCase(str : String) : Boolean {
  fun String.all(f : (Char) -> Boolean) : Boolean {
    for(val c in this) {
      if(!f(c)) {
        return false
      }
    }

    return true
  }

  return str.all {
    Character.isUpperCase(it)
  }
}

fun main(args: Array<String>) {
    println(isUpperCase("HI")) // 本来はtrueが返却されるはず。
}

なんてことのない普通のfor文に見えますが、for(val c in this) {'val' on loop parameter is not allowedというエラーが発生して実行できません。
初学者でしかもあまりプログラミングに慣れていない人にとってはこういった単純な落とし穴でも解決するまでに時間がかかると思いますので、注意が必要です。
ちなみにこちらのコードは以下のようにvalを取り除けば動きます。

isUpperCase
fun isUpperCase(str : String) : Boolean {
  fun String.all(f : (Char) -> Boolean) : Boolean {
    for(c in this) {
      if(!f(c)) {
        return false
      }
    }

    return true
  }

  return str.all {
    Character.isUpperCase(it)
  }
}

fun main(args: Array<String>) {
    println(isUpperCase("HI")) // trueが返却されます。
}

他にも中置き呼び出しのためにinfixが必要だったり、Intでtimesが使用できなくなったりと、0.x版からの変更点が多いので、初学者は注意が必要です。
では、どうするかという話ですが、手っ取り早く入門書を買うのが良いと思います。

Kotlinスタートブック - 新しいAndroidプログラミング

Null安全は重要なので必ず理解する。

Null安全に関する詳しい説明はこちら

何故かというと、Android StudioのKotlinプラグインを使えば、簡単にJavaファイルをKotlinにコンバートできる(プラグインをインストール後、[Code] → [Convert Java File to Kotlin File]を選択)のですが、元がJavaファイルということもあるのか、前のコードでnullチェックをしていないような変数にはNon-null(!!演算子)をコンバート時に指定されることがあるからです。

Non-nullは結構危険な演算子で、nullが代入されたときはそれだけで実行時例外(KotlinNullPointerException)を吐きます。

この辺りをコンバート時に自動で設定されてしまうので、あんまりKotlinを理解しないでコンバート処理にかけると、動かしてみたけどエラーで落ちる、といった痛い目にあいます。(今の私のようの)

Kotlinで宣言されたインタフェースにはSAM(Single Abstract Method)変換が働かない。

AndroidのOnClickListenarのような一つのインタフェースに実装するメソッドが一つの場合、SAM変換で匿名クラスにオーバーライドメソッドの宣言や引数を省略できます。

普通にjavaで書く場合
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.v(TAG, "クリックされました");
    }
});
Kotlinで書く場合
button.setOnClickListener { Log.v(TAG, "クリックされました") }

button.setOnClickListenerは
android.view.View.OnClickListenerなので、Javaで実装されたインターフェースについては問題なく動作するのですが、Kotlinで書かれたインタフェースの場合はSAM変換が働かないです。

Kotlinで定義したインタフェース
interface OnStringUpdateListener {
    fun onStringUpdate(str: String)
}
コンパイルエラーになるケース
private var mStr: String? = null
private var mStringUpdateListener: OnStringUpdateListener? = null

// SAM変換を期待しているがコンパイルエラーになる
mStringUpdateListener = OnStringUpdateListener{ str -> mStr = str }

何故なんだという話ですが、マニュアルにJavaインタフェースのみ対応と書かれてるので、Kotlinで記述したインタフェースではSAM変換は働きません。

じゃあ、このインタフェースは実装するにはどうすればいいのという話ですが、以下のように実装すればよろしいかと思います。

private var mStr: String? = null
private var mStringUpdateListener: OnStringUpdateListener? = null

mStringUpdateListener = object : OnStringUpdateListener {
    override fun onStringUpdate(str: String) {
        mStr = str
    }
}

うーん、なんかあんまりJavaと手間が変わらない。。。
というかKotlinならインタフェースを実装しなくてももっと効率のいい書き方ができる気がする。。。
⇒教えてもらいました。ありがとうございます。

private var mStr: String? = null
private var mStringUpdateListener: ((String) -> Unit)? = null
mStringUpdateListener = {
    mStr = it
}

まとめ

本家Java8でラムダ式が導入されましたが、Android開発で利用できるのはまだまだ先の話だと思います。
Kotlinでコードの記述量が減れば開発効率が上がるのかなとは思いますが、導入するかどうかはもう少しいじって判断したいかなぁと思います。