RecyclerViewの要素をクリックした時の処理をActivity・Fragmentでコールバックとして受けとる。
Android開発をやってると、かなりの頻度で書くことになるこの処理、みなさんはどのように書いていますか?
Interfaceによるコールバック
これは俺の推測でしかないのですが、おそらく一番一般的というか多数派なのが
クリック処理のコールバックを書いたinterfaceを実装
↓
interfaceをActivityに継承し、RecyclerViewのAdapterに渡す
この書き方だと思います。
interface TestListener {
/**
* RecyclerViewのセルのボタンをクリックしたら
*/
fun onClickTestButton(text: String)
}
class TestAdapter(var list: List<String>, private val listener: TestListener): RecyclerView.Adapter<TestAdapter.ViewHolder>() {
/**
* セルのインスタンス作成
* @param parent
* @param viewType
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ItemTestBinding.inflate(LayoutInflater.from(parent.context), parent, false).let {
TestAdapter.ViewHolder(it)
}
}
/**
* セルごとの処理
* @param holder
* @param position
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.apply {
testButton.text = list[position]
testButton.setOnClickListener { listener.onClickTestButton(list[position]) }
}
}
/**
* 表示するアイテムの数
*/
override fun getItemCount() = list.size
/**
* ViewHolder
* @param binding
*/
class ViewHolder(val binding: ItemTestBinding): RecyclerView.ViewHolder(binding.root)
}
class TestActivity: RxAppCompatActivity() , TestListener{
private lateinit var binding: ActivityTestBinding
private lateinit var adapter: TestAdapter
private var list = listOf("aaa", "bbb" ,"ccc") //RecyclerViewに表示させる適当な値
/**
* 初期化
* @param savedInstanceState
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_test)
initRecyclerView()
}
/**
* RecyclerViewの初期化
*/
private fun initRecyclerView() {
adapter = TestAdapter(list, this)
binding.testRecyclerView.apply {
layoutManager = LinearLayoutManager(context)
this.adapter = this@TestActivity.adapter
}
}
/**
* RecyclerView内のボタンがクリックされたら
* @param text
*/
override fun onClickTestButton(text: String) {
Log.v("クリック", "$text")
}
companion object {
/**
* Activity起動
* @param activity
*/
fun start(activity: Activity){
activity.startActivity(Intent(activity, TestActivity::class.java))
}
}
}
※今回はクラス名は基本的にTest〜という名前でつけてます
まあ、特に説明するような箇所もないと思いますが、testButtonってやつがクリックされた時の処理をInterfaceで定義してあげなきゃいけないのがちょっと面倒かなと思います。しかもRecyclerViewのクリック処理ってかなり頻出するので、何度も書いているうちに嫌気がさして来ますよね。
高階関数によるコールバック
そこでおすすめしたいのが今回紹介する二つ目の手法。高階関数を渡して処理を移譲するやり方。
このやり方を使うとInterfaceの実装が不要になるのでそれだけコードの量が減ります。今回はAdapterクラスのコンストラクタで高階関数を渡してみたいと思います。
class TestAdapter(var list: List<String>, private val onClickCallBack: (String) -> Unit): RecyclerView.Adapter<TestAdapter.ViewHolder>() {
/**
* セルのインスタンス作成
* @param parent
* @param viewType
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ItemTestBinding.inflate(LayoutInflater.from(parent.context), parent, false).let {
TestAdapter.ViewHolder(it)
}
}
/**
* セルごとの処理
* @param holder
* @param position
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.apply {
testButton.text = list[position]
testButton.setOnClickListener { onClickCallBack(list[position]) }
}
}
/**
* 表示するアイテムの数
*/
override fun getItemCount() = list.size
/**
* ViewHolder
* @param binding
*/
class ViewHolder(val binding: ItemTestBinding): RecyclerView.ViewHolder(binding.root)
}
class TestActivity: RxAppCompatActivity(){
private lateinit var binding: ActivityTestBinding
private lateinit var adapter: TestAdapter
private var list = listOf("aaa", "bbb" ,"ccc") //RecyclerViewに表示させる適当な値
/**
* 初期化
* @param savedInstanceState
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_test)
initRecyclerView()
}
/**
* RecyclerViewの初期化
*/
private fun initRecyclerView() {
adapter = TestAdapter(list, onClickTestButton) //ここで関数オブジェクトを渡す
binding.testRecyclerView.apply {
layoutManager = LinearLayoutManager(context)
this.adapter = this@TestActivity.adapter
}
}
/**
* RecyclerView内のボタンがクリックされたら呼ばれる
*/
private val onClickTestButton: (String) -> Unit = {
Log.v("クリック", "$it")
}
companion object {
/**
* Activity起動
* @param activity
*/
fun start(activity: Activity){
activity.startActivity(Intent(activity, TestActivity::class.java))
}
}
}
さっきよりもコード量が減りました。
このように、高階関数(関数オブジェクト)を渡す方法だとInterfaceの実装が不要になります。せっかくKotlinは高階関数が使えるので利用しない手はないですね。
番外編:RxJavaを使ったコールバック
最後に、RxJavaを使ったクリック処理のコールバックの実装方法を紹介したいと思います。
Adapter内にPublishSubjectを定義してあげて、クリックされる旅にonNextで流してあげるようにします。あとは、ActivityからそのSubjectを購読すればコールバックを受け取れるようになります。実際に実装をみながら確認してみます。
class TestAdapter(var list: List<String>): RecyclerView.Adapter<TestAdapter.ViewHolder>() {
/**
* クリックした際のSubject
*/
val onClickSubject: PublishSubject<String> = PublishSubject.create()
/**
* セルのインスタンス作成
* @param parent
* @param viewType
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ItemTestBinding.inflate(LayoutInflater.from(parent.context), parent, false).let {
TestAdapter.ViewHolder(it)
}
}
/**
* セルごとの処理
* @param holder
* @param position
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.apply {
testButton.text = list[position]
testButton.setOnClickListener { onClickSubject.onNext(list[position]) }
}
}
/**
* 表示するアイテムの数
*/
override fun getItemCount() = list.size
/**
* ViewHolder
* @param binding
*/
class ViewHolder(val binding: ItemTestBinding): RecyclerView.ViewHolder(binding.root)
}
class TestActivity: RxAppCompatActivity(){
private lateinit var binding: ActivityTestBinding
private lateinit var adapter: TestAdapter
private var list = listOf("aaa", "bbb" ,"ccc") //RecyclerViewに表示させる適当な値
/**
* 初期化
* @param savedInstanceState
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_test)
initRecyclerView()
observableOnClickRecyclerView()
}
/**
* RecyclerViewの初期化
*/
private fun initRecyclerView() {
adapter = TestAdapter(list)
binding.testRecyclerView.apply {
layoutManager = LinearLayoutManager(context)
this.adapter = this@TestActivity.adapter
}
}
/**
* RecyclerViewのクリックを監視
*/
private fun observableOnClickRecyclerView() {
adapter.onClickSubject.subscribe {
Log.v("クリック", it)
}
}
companion object {
/**
* Activity起動
* @param activity
*/
fun start(activity: Activity){
activity.startActivity(Intent(activity, TestActivity::class.java))
}
}
}
このように、Rxを使うとAdapterのコンストラクタにコールバックを渡す必要すらなくなります。個人的にはこの書き方が一番好きですが、良い書き方なのかどうかはなんとも言えないです。
今回紹介した3つの書き方のうち、それぞれのメリットやデメリットなどありましたらぜひ教えてください。
また、間違った説明をしている箇所などがありましたらどんどん教えていただけると嬉しいです。
最後まで読んでいただきありがとうございました。