Android(Kotlin) RecyclerView で PopupMenu(データバインディング方式)

2021/2/2

はじめに

各概念毎のサンプルコードはよく見かけても、複数組み合わせた場合のサンプルコードってあまりなかったりしますよね。今回は RecyclerViewPopupMenuデータバインディングを組み合わせたのですが、多少試行錯誤したので、まとめておきます。

前提

実装する機能は、

「RecyclerView の各要素内ボタンをタップで PopupMenu が開き、メニュー項目を選択すると選択したメニュー項目と対応要素に応じた処理が呼ばれる」

です。ただし、前述の通り、データバインディングな実装にします。

なお、補足ですが、後述するサンプルコードはグループメンバー一覧を扱うもので、命名がそれっぽくなっていますが、本質的な点では無いので気にしないでください。

実装

イベントハンドラー

リスナーバインディングに使うクラスを用意します。PopupMenu を直接扱うクラスになっています。他でも使い回せるように汎用的に作ってみました。

import android.view.MenuItem
import android.view.View
import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu

class PopupMenuHandler<T>(
    @MenuRes
    private val menuRes: Int,
    private val onPopupMenuItemClick: (T, MenuItem) -> Boolean,
) {
    fun onMenuClick(view: View, id: T) {
        val popupMenu = PopupMenu(view.context, view)
        popupMenu.menuInflater.inflate(menuRes, popupMenu.menu)
        popupMenu.show()
        popupMenu.setOnMenuItemClickListener { menuItem ->
            onPopupMenuItemClick(id, menuItem)
        }
    }
}

アダプタ

コンストラクタ引数として上記クラスのインスタンスを受け取り、データバインディングに使います。

import android.view.*
import androidx.recyclerview.widget.RecyclerView

class GroupMemberListAdapter(
    private val popupMenuHandler: PopupMenuHandler<String>,
) : RecyclerView.Adapter<GroupMemberListAdapter.ViewHolder>() {


    class ViewHolder(val binding: GroupMemberListViewItemBinding) :
        RecyclerView.ViewHolder(binding.root)


    private var items = emptyList<ListItem>()


    override fun getItemCount(): Int = items.size

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = ViewHolder(
        GroupMemberListViewItemBinding.inflate(
            LayoutInflater.from(parent.context), parent, false
        )
    )

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = items[position]
        holder.binding.also {
            it.item = item
            it.popupMenuHandler = popupMenuHandler
        }
    }

    fun setItems(members: List<ListItem>) {
        items = members
        notifyDataSetChanged()
    }
}

要素レイアウト(関連箇所のみ抜粋)

該当ボタンのクリックイベントをバインドします。

<ImageButton
    android:id="@+id/image_button_popup_menu"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_baseline_more_vert_24"ription_list_view_item_menu"
    android:onClick="@{(view) -> popupMenuHandler.onMenuClick(view, item.userGroupId)}"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    />

ちなみに、データバインディングにジェネリクスクラスを使う場合、何も考えないで記述すると、要素タイプ"variable"に関連付けられている属性"type"の値には、'<'文字を含めることはできません。とエラーが出ます。Android Studio の提案に従い、エスケープしておきましょう。

<data>
    <variable name="item" type="jp.re_arc_lab.app.feature.group_member.ListItem" />
    <variable name="popupMenuHandler" type="jp.re_arc_lab.app.utility.recyclerview.PopupMenuHandler&lt;String>" />
</data>

呼び出し元フラグメント(関連箇所のみ抜粋)

適切な引数でイベントハンドラをインスタンス化しつつ、アダプタをインスタンス化する際にそのイベントハンドラを渡します。onPopupMenuItemClick メソッドと併せて、となるので R.menu.group_member_popup の内容は割愛します。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    // 途中省略

    val groupMemberListAdapter = GroupMemberListAdapter(
        PopupMenuHandler(R.menu.group_member_popup, ::onPopupMenuItemClick),
    )

    // 途中省略
}


private fun onPopupMenuItemClick(userGroupId: String, menuItem: MenuItem): Boolean {
    TODO("ポップアップメニュー選択時の処理を実装")
}

おわりに

まあまあスッキリ記述できましたかね。いかがでしょうか。