AndroidでListViewと関数オブジェクトを使ったメニュー画面を作る。

# はじめに

以下のようなListViewを使ったメニュー画面を作るときに、
項目ごとに実行される処理が異なるので、
その処理部分を関数オブジェクト化してみたら、
一元管理できて便利だったのでまとめておきます。


<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19321/90d56e2b-bc36-2e70-3f45-159f37aa425e.png"/>

## レポジトリ

https://github.com/cnaos/ListMenuExample

## 利用技術

* DataBinding
* DataBinding-ktx(https://github.com/wada811/DataBinding-ktx)

## MainActivity


```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    // wada811さんのDataBinding-ktxを使っています。
    // DataBinding-ktxを使うとonCreaete内のbindingの初期化コードが不要になります。
    //
    // https://github.com/wada811/DataBinding-ktx
    //
    // private lateinit var binding: ActivityMainBinding

    private val binding by viewBinding { ActivityMainBinding.inflate(it) }

    companion object {
        // メニュー画面のリストに表示するラベルと
        // タップされたときに実行する処理の定義
        val listItems = listOf(
            MyMenuItem("Toast表示") { activity->
                Toast.makeText(activity, "Toast Test", Toast.LENGTH_LONG).show()
            },

            MyMenuItem("何もしない"),

            MyMenuItem("アプリ終了") { activity->
                activity.finish()
            }
        )
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

//        binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
//        binding.lifecycleOwner = this

        val adapter = MyMenuListAdapter(this, listItems)
        binding.listView.adapter = adapter

        // リスト項目がタップされたときの処理
        binding.listView.setOnItemClickListener { parent, view, position, id ->
            val menuItem = listItems[position]

            // menuItemにセットされている関数オブジェクトを実行する
            menuItem.executeBlock?.invoke(this)
        }
    }
}
```

Databindingの初期化にwada811さんのDataBinding-ktxを使っている以外は特別な事はしてません。

やってる事は以下の3つです。

1. ListView用のカスタムArrayAdapter(MyMenuListAdapter)を作る
2. ListViewにカスタムArrayAdapterをセットする
3. ListViewの項目がタップされた処理を設定する

### メニュー画面に表示するリストの定義部分


```kotlin:MainActivity_listItems
    companion object {
        // メニュー画面のリストに表示するラベルと
        // タップされたときに実行する処理の定義
        val listItems = listOf(
            MyMenuItem("Toast表示") { activity->
                Toast.makeText(activity, "Toast Test", Toast.LENGTH_LONG).show()
            },

            MyMenuItem("何もしない"),

            MyMenuItem("アプリ終了") { activity->
                activity.finish()
            }
        )
    }
```

companion objectに定義しているlistItemsがメニュー画面に表示するリストの定義です。
画面に表示されるラベルと、項目をタップした時に実行される処理をラムダで記述してあります。

### MyMenuItem

```kotlin:MyMenuItem.kt
class MyMenuItem(
    // リスト項目に表示されるラベル
    val label: String,

    // リスト項目をタップしたときに実行される処理
    val executeBlock: ((activity: Activity) -> Unit?)? = null
)
```
コンストラクタ引数executeBlockの型は関数オブジェクトで、
その関数オブジェクトの引数はActivityで、
その関数オブジェクトの戻り値はUnitです。


### ListViewのタップ処理

```kotlin:MainActivity_onCreate
        // リスト項目がタップされたときの処理
        binding.listView.setOnItemClickListener { parent, view, position, id ->
            val menuItem = listItems[position]

            // menuItemにセットされている関数オブジェクトを実行する
            menuItem.executeBlock?.invoke(this)
        }
```

ここが核心部分です。

ListViewの項目がタップされると、このリスナが呼び出されるので、
positionから対応するMyMenuItemを取り出して、
MyMenuItemのインスタンスに定義されている関数オブジェクトを実行しています。
関数オブジェクトの引数thisにはactivityを渡しています。

### MyMenuListAdapter

```kotlin:MyMenuListAdapter
class MyMenuListAdapter(
    context: Context,
    menuList: List<MyMenuItem>,
    private val onClickListener: ((View, MyMenuItem) -> Unit)? = null
) : ArrayAdapter<MyMenuItem>(context, 0, menuList) {

    private val inflater = LayoutInflater.from(context)

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val binding =
            if (convertView == null) {
                val tmpBinding: MenuListItemBinding = DataBindingUtil.inflate(
                    inflater,
                    R.layout.menu_list_item,
                    parent,
                    false
                )
                tmpBinding.root.tag = tmpBinding
                tmpBinding
            } else {
                convertView.tag as MenuListItemBinding
            }

        binding.menuItem = getItem(position)

        return binding.root
    }
}
```

ListView用のDataBindingを使ったArrayAdapterです。
ここは特別なことはやってません。

## 画面のlayout定義とかその他

### activity_main.xml(layout)

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <ListView
            android:id="@+id/list_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

ListViewが1つあるだけの画面です。

### menu_list_item.xml(layout)

```xml:menu_list_item
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="menuItem"
            type="io.github.cnaos.example.listmenuexample.MyMenuItem" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/menu_label"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="16dp"
            android:text="@{menuItem.label}"
            android:textSize="18sp" />
    </LinearLayout>
</layout>
```

ListViewの各項目の表示に使われるlayoutの定義です。