araki tech

for developers including me

Android KotlinのonClickListnerを紐解く【初学者向け】

Android KotlinのonClickListnerを紐解く【初学者向け】

AndroidのonClickListener

みなさんAndroidで以下のようなコードありますよね。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val textView: TextView = findViewById(R.id.main_text)
        val button: Button = findViewById(R.id.main_button)

        button.setOnClickListener {
            textView.text = "ボタンが押された!"
        }
    }
}

今回、注目するのは、

button.setOnClickListener {
    textView.text = "ボタンが押された!"
}

の部分です。

少しずつ紐解いていきます。

何をしているのか

Buttonの親クラスであるViewクラスのメソッド、onClickListener()を呼び出しているのですが、実際には丸括弧 ( ) ではなく波括弧 { }での呼び出しをしていますね。

知っている人にとっては「そこから解説するのか」と思うかもしれませんが、そうでない人もいると思うので解説します。

これを理解するには、引数とラムダの関係を知る必要があります

関数の引数とラムダ

メソッド (関数) の引数の中で、最後のものをラムダ式で受け取る場合は、波括弧 { }で外に出してその中身を記述することができます。

例えば以下のようなコードだと分かりやすいでしょうか。

これから紹介するコードは全て、do something とコンソール出力されるKotlinコードです。

ラムダのSample その1

/** 何も引数を取らない、かつ何も返さない関数(ラムダ) [listener] を引数にとる */
fun doSomething(listener: ()->Unit) {
    listener()  // ラムダは関数のように呼び出すことができる
}

// { }で中身をその場で書いて渡す
doSomething {
    println("do something")
}

もちろん引数名はlistenerでなくても良いです。

そしてただの関数の引数なので以下のようにも書けます。

/** 何も引数を取らない、かつ何も返さない関数(ラムダ) [listener] を引数にとる */
fun doSomething(listener: ()->Unit) {
    listener()  // ラムダは関数のように呼び出すことができる
}

// 先にラムダを変数定義して渡す
val listener: ()->Unit = {
    println("do something")
}

doSomething(listener)

ラムダのSample その2

もし、ラムダに引数がある場合は以下のようになります。

/** Stringを一つ引数に取り、何も返さない関数(ラムダ) [listener] を引数にとる */
fun doSomething(listener: (String)->Unit) {
    listener("do something") // ラムダは関数のように呼び出すことができる
}

// 引数を一つ取るラムダはデフォルトで it が名前で割り当てられる
doSomething { 
    println(it)
}

// ラムダ内の引数名は自分でも決められる
doSomething { message ->
    println(message)
}

ここまでで、基礎的な知識はOKです。

本題に入ります。

onClickListenerの仕組み

onClickListenerButtonクラスのメソッドではなく、実際には親クラスにあたるViewのメソッドです。

定義を見てみると、

// View.java
public void setOnClickListener(@Nullable View.OnClickListener l) {
    // ...
}

となっています。

よく見てみると、引数はラムダではなくView.OnClickListenerという型 (クラス) を求めています。

なぜラムダのように { }で外側に書けたのでしょうか?

SAM – Single Abstract Method

答えはSAMという機能を使っています。

View.OnClickListenerは定義を見てもわかるように、interfaceです。

// View.java
public interface OnClickListener {
    void onClick(View var1);
}

ここで、メソッドがひとつしかないinterfaceSAM (Single Abstraction Method) interfaceもしくは Functional interfaceと呼びます。(以下省略してSAMと書きます)

SAMは特別なinterfaceで、SAMを引数にとるメソッドはラムダのような記述が可能になります。

実際に簡易的なコードで確認してみましょう。

/** 超シンプルなSAM */
fun interface SAMSample {
    fun doSomething()
}

/** SAMを引数に取る関数 */
fun callSample(samSample: SAMSample) {
    samSample.doSomething()
}

// 呼び出し側
callSample {
    println("do something")
}

KotlinではSAMインターフェースは明示的にfun interfaceで定義します。

これは、オブジェクト式を使った以下のようなコードとも同義になります。

callSample(object : SAMSample {
    override fun doSomething() {
        println("do something")
    }
})

この書き方はSAMでは冗長なので基本的にやるメリットはないです。

しかし、以下のような書き方は実装をファイル分けできるので、やる意味はあります。

もしラムダの中が複雑で長くなるようであれば以下の方法を取ることも選択肢に入れましょう。

/** 実際にinterfaceを継承して実装する */
class MySample : SAMSample {
    override fun doSomething() {
        // ...
        println("do something")
        // ...
    }
}

// 呼び出し側
callSample(MySample())

このやり方は実装部分を再利用可能であるメリットもありますが、基本的にラムダを使ってスマートに書くことが多いです。

余談 : onClickListenerもこう書ける

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val textView: TextView = findViewById(R.id.main_text)
        val button: Button = findViewById(R.id.main_button)

        button.setOnClickListener(MyTextOnClickListener(textView))
    }
    
    class MyTextOnClickListener(val textView: TextView) : View.OnClickListener {
        override fun onClick(v: View?) {
            textView.text = "ボタンが押された!"
        }
    }
}