初めてのkotlin

kotlinによるAndroidアプリ開発での気付き

ボタンのマークをテキストの右に置く

kotlinの話ではないですが...(^^;

チェックボックスラジオボタンも、 普通に設置するとマークはテキストの左に表示されます。

これを右にするための設定です(必要な部分だけ)。

    <!-- チェックボックスの場合 -->
    <CheckBox
        ・・・
        android:button="@null"
        android:drawableRight="?android:attr/listChoiceIndicatorMultiple"
    />

    <!-- ラジオボタンの場合 -->
    <RadioButton
        ・・・
        android:button="@null"
        android:drawableRight="?android:attr/listChoiceIndicatorSingle"
    />

オプションメニューにボタンを付ける

オプションメニューに、チェックボックスラジオボタンを付けます。 また、ラジオボタンはサブメニュー化します。

注意点としては、選択状態の切替えは、自分でプログラムを書くこと。 何も書かないと、クリックしても選択状態にはなりません。

res\menu\menu_main.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <!-- チェックボックス -->
    <item
        android:id="@+id/action_check1"
        android:title="@string/action_check1"
        android:checkable="true"
        app:showAsAction="never" />

    <!-- サブメニュー -->
    <item android:id="@+id/action_submenu"
        android:title="@string/action_submenu"
        app:showAsAction="never">
        <menu>
            <!-- ラジオボタン -->
            <group android:checkableBehavior="single">
                <item android:id="@+id/action_radio1"
                    android:title="@string/action_radio1" />
                <item android:id="@+id/action_radio2"
                    android:title="@string/action_radio2" />
            </group>
        </menu>
    </item>

</menu>

menuフォルダが無ければ作ってください。

次にActivityのクラス内に、設置と押された時の処理を書きます。

class MainActivity : AppCompatActivity() {
    ・・・
    //オプションメニューの表示
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        val inflater = menuInflater
        inflater.inflate(R.menu.menu_main, menu)

        //チェックボックスの初期化
        menu?.findItem(R.id.action_check1)?.setChecked(true)

        //ラジオボタンの初期化
        menu?.findItem(R.id.action_radio1)?.setChecked(true)

        return true
    }
    //押された時の処理
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {

            //チェックボックスの切替
            R.id.action_check1 -> {
                item.setChecked(!item.isChecked())
                ・・・
                true
            }

            //ラジオボタンの選択
            R.id.action_radio1 -> {
                item.setChecked(true)
                ・・・
                true
            }
            R.id.action_radio2 -> {
                item.setChecked(true)
                ・・・
                true
            }

            else -> super.onOptionsItemSelected(item)
        }
    }
}

オプションメニューを設置する

アプリバーとかアクションバー、ツールバーなどと呼ばれるタイトルバー部分に、 オプションメニューを付ける方法です。

まずは、リソースデータから。

res\menu\menu_main.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/action_item1"
        android:title="@string/action_item1"
        app:showAsAction="never" />
</menu>

※menuフォルダが無ければ作ってください。 (Markdown記法のファイル名表記には対応してないのかな?)

次にActivityのクラス内に、設置と押された時の処理を書きます。

class MainActivity : AppCompatActivity() {
    ・・・
    //オプションメニューの表示
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        val inflater = menuInflater
        inflater.inflate(R.menu.menu_main, menu)
        return true
    }
    //押された時の処理
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_item1 -> {
                ・・・
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }
}

Error inflating class

コンパイルは通るのに、実行時に落ちる。

java.lang.RuntimeException: Unable to start activity ~: Binary XML file line ~: Error inflating class ~

デバッガで確認してみると、リソースが原因で落ちている模様。

ネットで調べてみると、リソースがみつからないか、 コンストラクタが不十分と言ったことが原因のようです。

リソースは、端末によって自動的に切り替えられるようになっているので、 例えば、drawable-v24フォルダのように、 特定のバージョンでなければ利用されない場所にあるリソースは、 バージョンが足りない環境で参照しようとすると落ちます。

また、カスタムビューをクラス名だけで指定している場合も、 コンパイルは通りますが実行時に落ちますので、 下のように完全修飾クラス名で指定すること。

    <com.xxx.myapp.CustomView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        />

Viewのコンストラクタについては、 前にも書いたとおり、 @JvmOverloadsアノテーションを付けていれば大丈夫です。

イベントリスナの書き方でthisの意味が変わる

kotlinでは、SAM変換を利用してシンプルな形でイベントリスナを記述できますが、 正規の書き方で書いた時とシンプルな形で書いた時で、 thisが違うものを指します。

今回、イベント内でthisを使う必要が出て、気付きました。

kotlin流
    button.setOnClickListener{
        Log.e("debug","button SAM " + this.javaClass + " " + it.javaClass)
    }
    //以下のように表示される
    //E/debug: button SAM class ~.MainActivity class ~.AppCompatButton

イベントを登録したクラスのインスタンスが示されています。

Java
    button.setOnClickListener(
        object : View.OnClickListener {
            override fun onClick(p0: View?) {
                Log.e("debug","button " + this.javaClass + " " + p0?.javaClass)
            }
        }
    )
    //以下のように表示される
    //E/debug: button class ~.MainActivity$initView$2 class ~.AppCompatButton

リスナのインスタンスが示されています。

thisが必要な時は?

Viewのイベントを上書きして、それを取り消す時に必要でした。

        view.getViewTreeObserver().addOnPreDrawListener({
            view.getViewTreeObserver().removeOnPreDrawListener(this)
            true
        })

ここではリスナのインスタンスが欲しいのに、 クラスのインスタンスが入ってるので、 コンパイルエラーになります。

逆に、クラス内に用意した変数にアクセスできるので、便利な 面もありますが(^^;

クラスの書き方

カスタムViewの書き方
class CustomView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
    ・・・
    init {
        ・・・
    }
}

上記サンプルはRelativeLayoutですが、この部分は継承するViewに変えればOKです。 ポイントは@JvmOverloadsアノテーション。 これにより、おまじないのようなconstructorが省略できます。

初期化処理はinitブロックで。

データクラスの場合
data class SampleData(
    var value1: Int = 0,
    var value2: Int = 0,
    var value3: String = ""
) {
    ・・・
    init {
        ・・・
    }

    constructor(value4: Int, value3: String) : this(value4, 1, value3) {
        ・・・
    }

}

クラス定義の仮引数部分が、var(またはval)を用いた変数宣言になっているのがポイント。 これが、変数宣言とprimary constructorの役割を果たすようです。 また、初期値を設定しておくことで、引数を省略して呼び出すことも可能になります。

もちろん、サンプルに記載のとおり、別途constructorを追加することも可能。 注意としては、必ずthis()によってprimary constructorが呼び出されなければなりません。

この辺りが、Javaとは異なる部分のようです。

Activityの場合
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ・・・
    }
    ・・・
}

Activityに関しては、カスタムViewとは違い、 onCreateメソッド内で初期化するようです。

イベントリスナ(コールバック)の書き方

Java風の書き方もできますが、

interface OnSampleListner {
    fun onSample(int: Int)
}

class Sample {
    var sampleListner: OnSampleListner? = null

    fun sampleMethod() {
        sampleListner?.onSample(1)
    }
}

fun main() {
    val sample1 = Sample()

   sample1.sampleListner = object: OnSampleListner {
        override fun onSample(int: Int) {
            println("Event!! $int")
        }
    }

    sample1.sampleMethod()    //Event!! 1
}

kotlinでは、一部のリスナに対してはSAM変換によって、 とてもシンプルに定義できるようで、

button.setOnClickListener {
     println("Event!!")
}

自前のコールバックでも、同様の書き方ができないか調べていたら、 interfaceの代わりにtypealiasを使う方法がありました。

typealias OnSampleListner2 = (Int) -> Unit

class Sample2 {
    var sampleListner: OnSampleListner2? = null

    fun sampleMethod() {
        sampleListner?.invoke(2)
    }
}

fun main() {
    val sample = Sample2()

    sample.sampleListner = { int ->
        println("Event!! $int")
    }
    sample.sampleListner = {    //引数がひとつ以下の時は、こうも書ける
        println("Event!! $it")
    }

    sample.sampleMethod()    //Event!! 2
}

また、汎用性を求めなければ、typealiasすら省略できるようです。

class Sample3 {
    var sampleListner: ((int: Int) -> Unit)? = null

    fun sampleMethod() {
        sampleListner?.invoke(3)
    }
}

fun main() {
    val sample = Sample3()

    sample.sampleListner = { int ->
        println("Event!! $int")
    }
    sample.sampleListner = {    //引数がひとつ以下の時は、こうも書ける
        println("Event!! $it")
    }

    sample.sampleMethod()    //Event!! 3
}

書き方によって、thisの意味が変わることが判明したので、 注意としてまとめました。

kotlinhint.hatenablog.jp