はじめに
Kotlinかわいい!
ではなく、Javaだとリスナーやコールバックの処理の記述が長くなってしまいますが、Kotlinだと短くスッキリ書ける。というお話です。
プログラミング言語KotlinはJVM言語の一つです。
「Kotlinって何?興味ある!」という方は、とてもわかりやすいこちらのページなどをお読み下さい。
Javaのリスナーやコールバックは長くなってしまう
Kotlinの話は一回置いておいて、次のJavaのコードを見てください。
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.v(TAG, "clicked");
}
});
このAndroidアプリケーションのコードは、ボタンをクリックしたらログを表示するリスナーを設定しています。
ここでは、ViewというクラスのsetOnClickListenerというメソッドを呼び出しています。
このメソッドは、View.OnClickListenerというインターフェースを引数にとります。
ここでは、View.OnClickListenerインターフェースを実装した匿名クラスを生成して、それを引数に渡しています。
自分は、このコードが好きではありません。
本質的な処理が、あまり大切でないコードで埋もれてしまっているからです。
ここで本質的なものは何でしょうか?自分は次の3個だと思います。
- buttonという変数に対して
- Logを表示する
- リスナーをセットする
先ほどのコードは本質でなく冗長な部分が多いように感じます。
button.setOnClickListener(new View.OnClickListener() { // new以降本質でない
@Override // 本質でない
public void onClick(View v) { // あまり本質でない
Log.v(TAG, "clicked");
} // 本質でない
}); // 本質でない
コード中にコメントもしましたが、
- new View.OnClickListenerという記述
- Overrideアノテーション
- onClickメソッドというメソッド名
-
{}
や()
は本質ではないと思います。
ですが、上記のコードはJavaの文法上書く必要があります。(Overrideアノテーションは書かなくてもいいですが。)
Androidアプリケーション開発だけでなく、Javaの開発ではこのようなSAM型のインターフェースをUIのイベントリスナー・非同期処理のコールバックとして用いることが多いです。
さきほどのコードは5行ですが、実際のコードを考えてみます。いくつもあるボタンなどのUIにリスナーを設定したり、非同期の処理が複数絡み合ったりして、複数のリスナー・コールバックが必要になることがあります。そのような場合、冗長なボイラープレートコードにより大切なコードが圧迫されてしまい、本質的な処理が追いづらくなってしまいます。
実は、この節とほぼ同じ節をもつ投稿をちょっと前にしました。
よろしければこちらもご覧下さい。
「Kotlin興味あるけれど大人の事情で使えない」という方、IntelliJやAndroidStudioなら、このようなリスナー・コールバックに関して、コードを読む際はスッキリ読めますよ。
Kotlinで書くとスッキリ
ここでKotlinの登場です。
先ほどのコードKotlinで書くとどのようになるのでしょうか。
button.setOnClickListener { Log.v(TAG, "clicked") }
びっくりするくらい短くなりました!
さて、View.OnClickListenerというインターフェース名や、onClick(View v)というメソッドはどこにいったのでしょうか。実はここでは関数リテラルを、SAMインターフェースに変換するSAM変換が行われています。
Kotlinのコード、どこがいいだろう?
SAM変換の前に、ちょっとだけ。
Javaで5行なものが、Kotlinだと1行になりました。スッキリしましたね。
スッキリしましたが、自分は行数が短くなったことがミソではない気がします。
Kotlinだと、
「冗長な部分は書く必要がなく本質的なことだけ書けばよくなった」
というのがミソだと思います。
Kotlinのコードを再掲します。
button.setOnClickListener { Log.v(TAG, "clicked") }
- buttonという変数に対して
- Logを表示する
- リスナーをセットする
という部分のみを記述しています。
ノイズになるような記述がなく、本質的なものだけ集中して読めますね。
SAMインターフェースとSAM変換
SAMとはSingle Abstract Methodの略で、SAMインターフェースは、一つだけ抽象メソッドをもつインターフェースです。SAM型のインターフェースのほんの一例を挙げると、次のようなものがあります。
- java.lang.Runnable
- java.lang.Comparable
- android.view.View.OnClickListener
- android.location.GpsStatus.Listener
さて、さきほどのKotlinのコードの次の部分では、関数リテラルを生成しています。
{ Log.v(TAG, "click") }
(Kotlinにおける)SAM変換とは、関数リテラルがSAM型のインターフェースに変換されることをいいます。
関数リテラルについての説明は以下のページが分かりやすいです。
- @ngsw_taro さんのアドベントカレンダー10日目:関数(後)
- JetBrinansによる公式ページ
(ちなみに、GroovyのSAM変換では、クロージャーがSAM型のインターフェースに変換されます。)
SAM変換、書き方いろいろ
先ほどの例以外の書き方も可能です。
次のコードでは、関数リテラルの型を明示しています。
button.setOnClickListener( { (v : View): Unit -> Log.v(TAG, "clicked") })
View.OnClickListenerインターフェースの唯一のメソッドonClick(View v)は、引数はView型、返値型はvoidです。これからViewを貰ってUnit(Javaのvoid)を返す関数リテラルと型推論が可能です。そのため次のような関数リテラルの型を書かない書き方もできます。
button.setOnClickListener( { v -> Log.v(TAG, "clicked")})
さて、Kotlinではメソッドの最後の引数が関数リテラルの場合次のように、括弧を省略することが可能です。
button.setOnClickListener{ v -> Log.v(TAG, "clicked") }
また、関数リテラルで引数が一つの場合、引数部分を省略することが可能です。
button.setOnClickListener{ Log.v(TAG, "clicked") }
つぎの例は、View->Unit型の関数リテラルを一度変数に入れて、それを引数に渡しています。
この書き方も可能です。
val listener : (View) -> Unit = {v -> Log.v(TAG, "clicked") }
// or
val listener : (View) -> Unit = {Log.v(TAG, "clicked")
// or
val listener = { (v : View) : Unit -> Log.v(TAG, "clicked") }
button.setOnClickListener(listener)
SAM変換できそうでできない例
さて、次の書き方は一見SAM変換できるように見えますが、コンパイルエラーになってしまいます。
val listener : View.OnClickListener = { (v : View) : Unit -> Log.v(TAG, "clicked") }
メッセージを見ると、「Type mismatch」と出ます。
次にas演算子でキャストできないか試してみました。
val listener = { (v : View) : Unit -> Log.v(TAG, "clicked") } as View.OnClickListener
こちらは、java.lang.ClassCastExceptionが発生し、実行時エラーになってしまいました。
どうやら、JavaのSAM型のインターフェースを引数にとるメソッドに、関数リテラルを渡した場合のみ、変換が行われるようです。
もう一例、VolleyのStringRequestについて
「ボタンのリスナーをセットするコードが5行から1行になったって、そんな嬉しくない」、そう思った方のためにもう一例。
Androidのネットワークライブラリ、VolleyのStringRequestのインスタンスを生成するコードの例を載せます。
まずはJava版から。
StringRequest request = new StringRequest(
"https://www.google.co.jp/",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Toast.makeText(getApplicationContext(), response, Toast.LENGTH_LONG).show();
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError volleyError) {
Toast.makeText(getApplicationContext(), "onErrorResponse", Toast.LENGTH_LONG).show();
}
}
);
処理のコールバックの部分が(インターフェースを実装した部分)に無駄な部分が多く、無駄に縦に長くなってしまいます。
大切な処理が埋もれてしまっています。
次にKotlinで書き換えます。関数リテラルとSAM変換を用います。
val request = StringRequest(
"https://www.google.co.jp/",
{ response ->
Toast.makeText(this, response, Toast.LENGTH_LONG).show()
},
{ volleyError ->
Toast.makeText(this, "onErrorResponse", Toast.LENGTH_LONG).show()
}
);
無駄な部分が無くなってスッキリしました。
スッキリしましたが、Response.ListenerやResponse.ErrorListenerインターフェース名、そしてonResponse(String response)やonErrorResponse(VolleyError volleyError)が無くなったため、「この関数リテラルが何に使われるか詳しく分からない」とも思えます。
そのような場合、名前付き引数でメソッドを呼び出すことで可読性を補うことができます。
val request = StringRequest(
url = "https://www.google.co.jp/",
listener = { response ->
Toast.makeText(this, response, Toast.LENGTH_LONG).show()
},
errorListener = { volleyError ->
Toast.makeText(this, "onErrorResponse", Toast.LENGTH_LONG).show()
}
);
まとめ
Javaだと冗長になりがちな、リスナーやコールバックのコードの記述。
Kotlinだと関数リテラルとSAM変換を使って、スッキリ書くことができますね!
おまけ、SAM変換を使わない書き方
次のような関数リテラルとSAM変換を使わない書き方もできます。
button.setOnClickListener(object: View.OnClickListener {
public override fun onClick(v: View) {
Log.v(TAG, "clicked")
}
});
関連リンク・参考リンク
- Kotlin(公式)
- QiitaのKotlinの投稿
- Kotlin Advent Calendar 2012 (全部俺)(@nsgw_taro さん)
- Kotlinの文法メモ(無名オブジェクト、override、thisキーワード(@shoma2da さん)