Kotlinでリスナーやコールバックをスッキリと書く【関数リテラルとSAM変換】

  • 92
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

Kotlinかわいい!
ではなく、Javaだとリスナーやコールバックの処理の記述が長くなってしまいますが、Kotlinだと短くスッキリ書ける。というお話です。

プログラミング言語KotlinはJVM言語の一つです。
「Kotlinって何?興味ある!」という方は、とてもわかりやすいこちらのページなどをお読み下さい。

Javaのリスナーやコールバックは長くなってしまう

Kotlinの話は一回置いておいて、次のJavaのコードを見てください。

Android、ボタンをクリックしたらログを表示する
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を表示する
  • リスナーをセットする

先ほどのコードは本質でなく冗長な部分が多いように感じます。

Android、ボタンをクリックしたらログを表示する(コメント付き)
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で書くとどのようになるのでしょうか。

Android、ボタンをクリックしたらログを表示する(Kotlin版)
button.setOnClickListener { Log.v(TAG, "clicked") }

びっくりするくらい短くなりました!

さて、View.OnClickListenerというインターフェース名や、onClick(View v)というメソッドはどこにいったのでしょうか。実はここでは関数リテラルを、SAMインターフェースに変換するSAM変換が行われています。

Kotlinのコード、どこがいいだろう?

SAM変換の前に、ちょっとだけ。

Javaで5行なものが、Kotlinだと1行になりました。スッキリしましたね。
スッキリしましたが、自分は行数が短くなったことがミソではない気がします。
Kotlinだと、

「冗長な部分は書く必要がなく本質的なことだけ書けばよくなった」

というのがミソだと思います。

Kotlinのコードを再掲します。

Android、ボタンをクリックしたらログを表示する(Kotlin版再掲)
button.setOnClickListener { Log.v(TAG, "clicked") }
  • buttonという変数に対して
  • Logを表示する
  • リスナーをセットする

という部分のみを記述しています。
ノイズになるような記述がなく、本質的なものだけ集中して読めますね。

SAMインターフェースとSAM変換

SAMとはSingle Abstract Methodの略で、SAMインターフェースは、一つだけ抽象メソッドをもつインターフェースです。SAM型のインターフェースのほんの一例を挙げると、次のようなものがあります。

さて、さきほどのKotlinのコードの次の部分では、関数リテラルを生成しています。

関数リテラル
{ Log.v(TAG, "click") }

(Kotlinにおける)SAM変換とは、関数リテラルがSAM型のインターフェースに変換されることをいいます。
関数リテラルについての説明は以下のページが分かりやすいです。

(ちなみに、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版から。

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変換を用います。

VolleyのStringRequest(Kotlin版)
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)が無くなったため、「この関数リテラルが何に使われるか詳しく分からない」とも思えます。

そのような場合、名前付き引数でメソッドを呼び出すことで可読性を補うことができます。

VolleyのStringRequest(Kotlin版名前付きと共に)
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変換を使わない書き方もできます。

Kotlinで関数リテラルでないリスナーの書き方
button.setOnClickListener(object: View.OnClickListener {
    public override fun onClick(v: View) {
        Log.v(TAG, "clicked")
    }
});

関連リンク・参考リンク

関連投稿