hildsoftのコード置き場

プログラム関連で調べたことやコードの保管場所です

Kotlinでリスナーを書く方法とSAM変換

スポンサーリンク

Kotlinでのリスナーの書き方

Kotlinは少し触っている程度で未だにSAM変換の書き方を覚えられていないため、必要なところだけ自分なりにまとめてみます。

何が何でもSAM変換する必要は無いと思っています。あくまでソースの述量減少に伴う可読性を重視することが大事なので、 コーディング規約に従ってある程度の裁量を持たせても良いかと思います。


SAM変換とSAM変換が使える条件

SAM変換はSAM(Single Abstract Method)を定義したinterfaceを、ラムダ式に置き換えることです。

名前から分かる通り、メソッドを1個だけ定義したインターフェースをクラス化するときに記述量を減らせるだけなので、 複数のメソッドを定義しているインターフェースは従来通りの書き方になります。

あと注意点として、Javaで定義されているinterface限定になります。Kotlinでinterfaceを定義した場合は使えません。

何故そのような仕様になっているのかは分からないですが、今後Kotlinに開発の主軸が移ってきたら対応されるのでしょうか?


冗長な書き方

Kotlinを使っている人はJavaの方が慣れているという人も多いと思うので、Javaのコードから始めていきます。

AndroidでボタンをクリックするとLogを出力する例を書いてみます。

Java

AAAButton.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    Log.v("ログ出力");
  }
});

Kotlin

AAAButton.setOnClickListener( object: View.OnClickListener{
  override fun onClick(view: View?): Unit {
    Log.v("ログ出力")
  }
})

この書き方ではKotlinの省略化のメリットは無いですね。
しかしほぼ1:1で対応しているのでJavaとKotlinを対比する上では見やすいと思います。

SAM変換による省略化

AAAButton.setOnClickListener( object: View.OnClickListener{
  override fun onClick(view: View?): Unit {
    Log.v("ログ出力")
  }
})

このKotlinのコードをベースに進めていきます。

まずsetOnClickListenerに渡すパラメータは、View.OnClickListenerインターフェースを実装したクラスインスタンス ということは文法上自明ですね。

またKotlinでのラムダ式の記述は

{ パラメータ -> メソッドの処理 }

で関数名を省略して書けます。

View.OnClickListenerインターフェースにはonClickメソッドにしかないので、
View.OnClickListenerで実装されているメソッドは一つで、onClickのことだとメソッド名を書かなくても特定出来るわけです。
メソッドの特定ができれば、もちろんパラメータの型も戻り値の型も特定できます。

となると、

  • View.OnClickListener
  • onClick
  • onClickのパラメータの型(View)、戻り値の型(Unit)

は特になくても良い情報となります。削ってしまいましょう。

削る前

AAAButton.setOnClickListener( object: View.OnClickListener{
  override fun onClick(view: View?): Unit {
    Log.v("ログ出力")
  }
})

削った後

AAAButton.setOnClickListener( { view ->
  Log.v("ログ出力")
})

ここからはラムダ式の文法で、

  • 引数が一つであれば省略できて、処理部分で必要ならitとして使用できる
  • メソッド(ここではsetOnClickListener)の最後の引数がラムダ式なら、{}部分を後ろに書ける
  • その状態で()内に引数が無ければ、つまりメソッドの引数がラムダ式だけなら()を省略できる

を適用して省略していくと

AAAButton.setOnClickListener({ Log.v("ログ出力") })
AAAButton.setOnClickListener(){ Log.v("ログ出力") }
AAAButton.setOnClickListener{ Log.v("ログ出力") }

と書くことができます。


中身の少ない同じような記述を並列して定義する場合は、わざわざ指定のinterfaceを実装した内部クラスを作ったりする必要なくシンプルに書くことができます。

個人的には内部の処理が長かったり複雑だったりするとクラス分けたくなりますが、この辺はコーディング規約などでケースバイケースの対応になるかと思います。


サンプル

SAM変換を理解したところで本題。いくつかのパターンを書いておきます。

インターフェース内のメソッド一つ

リスナークラスだけをパラメータに取るメソッド

AAA.setOnXXXListener{ Log.v("ログ出力") }

リスナークラスだけをパラメータに取るメソッドでメソッドの引数を使用したい場合(itを使用)

AAA.setOnXXXListener{ it.BBB() }

リスナー側で実装を要求されるメソッドにパラメータが複数ある場合

AAAButton.setOnClickListener{ p1, p2 ->
  Log.v("ログ出力")
}

インターフェース内のメソッドが複数

  AAA.setOnXXXListener(object : BBBinterface {
    override fun onCCCMethod(p: DDDParameterClass?) {
      Log.v("ログ出力")
    }
    override fun onEEEMethod(p: FFFParameterClass?) {
    }
})

処理が長い場合

  num = 10
  AAA.setOnXXXListener(BBBClass(num))

・・・中略・・・

// 要求されるインターフェースを実装する内部クラス
inner class BBBClass(num: Int) : OnXXXListener {
    override fun onCCCMethod(p: DDDParameterClass?) {
      ・・・中略・・・
    }
}