Edited at

20行で簡易なRecyclerViewを実装できるKotlinのDSLを作ったので爆速開発しよう!(RoomとLiveDataを想定した更新処理付き)


モチベーション

RecyclerViewを最初から実装するときなんだかんだ言って面倒ですよね。

しかも簡単なものでさえ。(慣れもありそうですが)

極力小さい実装で動作確認したい!って時にも、一からってなると、ちょっと忘れていたりして、検索するとJavaで書かれていたり、単純にコードの行数が多かったり、ViewHolder、Adapterの実装、クラスの命名・・・やりたいこととのギャップが多くて、意外とストレスなんですよね。

僕も簡単なサンプルアプリをつくって、いろんなバリエーションのサンプルの実装を紹介したいって時がたまにあって、プロジェクト作成して、Androidxにマイグレーションして、そして毎回ここで、えーつらい・・・ってなる。(そして別のものに気がいく)

なのでそう言う労力減らしたいなーと思って簡単なコードで実装できるようにしてみました。


完成品

以下は毎回作るのに必要になるコードです。


MainActivity.kt

package com.exmample.sampleapp

import android.app.Activity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.exmample.sampleapp.data.entity.MyData
import com.exmample.sampleapp.data.repository.MyDataRepository
import com.exmample.sampleapp.data.room.AppDatabase
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.item_topic.view.*
// ここに今回のキモになる実装を入れてある
import com.exmample.sampleapp.shared.widget.*

val DATE_FORMATTER = SimpleDateFormat("yyyy MM/dd HH:mm:ss", Locale.getDefault())
fun Date.toFormatString(formatter: SimpleDateFormat) = formatter.format(this)

class MainActivity : AppCompatActivity() {
private val repository by lazy { MyDataRepository(AppDatabase.getInstance(thi.applicationContext).myDataDao()) }
// Roomから自動でデータ更新させる
private val listDataStream by lazy { repository.findAll() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initLayout(listDataStream)
}
}
// ネスト減らしたいって願望だけで拡張関数(ついでにActivityがもつ状態からも独立)
fun Activity.initLayout(dataStream: LiveData<List<MyData>>) {
// レイアウトファイルに指定したRecyclerViewのidからDSLの実装が生える!!
// RecyclerView
topic_list(linerLayoutManager()) {
// RecyclerView.Adapter
adapter(R.layout.item_topic) {
var list = mutableListOf<MyData>() //再代入させるターゲット
itemCount = { list.size }
dataStream.observe(this@initLayout, Observer { data ->
list = data
notifyDataSetChanged()
})
onBind { vh, pos -> // vh:ViewHolder
list[pos].let { (id, content, date) ->
vh.itemView.title.text = date.toFormatString(DATE_FORMATTER)
vh.itemView.description.text = content
// 画面遷移部分は別途いい感じに実装してください
vh.itemView.setOnClickListener { goNextScreen(id) }
}
}
} // RecyclerView.Adapter
} // RecyclerView
}


LiveDataを使わないで書いた時(何かしらサンプル実装をFragmentごとに用意したいみたいな時に使えそうなのでこのコードもいれた)


without_live_data

enum class Screen {

TOPIC01, TOPIC02, TOPIC03
}
fun Activity.initLayout() {
data class Column(val title: String, val description: String, val screen: Screen)

val list = listOf(
// Screenがタップされたときの遷移先を表す
Column("Topic 01", "これはトピック1です", Screen.TOPIC01),
Column("Topic 02", "これはトピック2です", Screen.TOPIC02),
Column("Topic 03", "これはトピック3です", Screen.TOPIC03)
)
// 拡張関数はtopic_list.invoke(と入力した後だとimportできるのでそのあとinvoke消す
topic_list(linerLayoutManager()) {
adapter(R.layout.item_topic) {
onBind { vh, pos ->
list[pos].let { (ttl, dsc, dest) ->
vh.itemView.title.text = ttl
vh.itemView.description.text = dsc
// 画面遷移部分は別途いい感じに実装してください
vh.itemView.setOnClickListener { goNext(dest) }
}
}
itemCount = { list.size }
}
}
}


一応ちゃんと動きます。

どうですか?たった20行でRecyclerViewを定義できました。

これならある程度簡単なモックならサクッと作れそうにみえないですかね?

(一応自分のアプリではちゃんと動いていますよ)


どうやってつくったか?

もうすでにKotlinをつかったDSLの実装について理解しておられるかたは、飛ばしてください。

別に特別なライブラリをつくったわけではないです。Kotlinのラムダ式をつかってシンプルなRecyclerViewのためのDSL(ドメイン特化言語)と言うものを作りました。

KotlinでのDSLの実装Tipsについては別の人がいろいろやっているので割愛しますが、公式のリンクくらいはのっけておきますね。

はいこれ→ https://kotlinlang.org/docs/reference/type-safe-builders.html

これをみて勉強して、HTMLのビルドと同じ要領でできるんだと理解できればいいです。


RecyclerViewのDSL実現するためにつくったもの

多少長くなりますが、70行いかないくらいです。


RecyclerView

package com.exmample.sampleapp.shared.widget

import android.app.Activity
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

typealias OnCreateViewHolder<VH> = (parent: ViewGroup, viewType: Int) -> VH
typealias GetItemCount = () -> Int
typealias OnBindViewHolder<VH> = (holder: VH, position: Int) -> Unit

object SimpleRecyclerView {
class Adapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
lateinit var makeViewHolder: OnCreateViewHolder<out RecyclerView.ViewHolder>
lateinit var itemCount: GetItemCount
lateinit var onBindListener: OnBindViewHolder<in RecyclerView.ViewHolder>
override fun onCreateViewHolder(p: ViewGroup, vt: Int): RecyclerView.ViewHolder = makeViewHolder(p, vt)
override fun getItemCount(): Int = itemCount()
override fun onBindViewHolder(vh: RecyclerView.ViewHolder, pos: Int) = onBindListener(vh, pos)
}
}

data class VH(val itemView: View) : RecyclerView.ViewHolder(itemView)

fun Activity.linerLayoutManager() = LinearLayoutManager(this)

// こいつのインポートが若干だるいです(多分)
operator fun RecyclerView.invoke(
loMgr: RecyclerView.LayoutManager,
f: RecyclerView.() -> Unit
) {
layoutManager = loMgr; f()
}

fun RecyclerView.adapter(
@LayoutRes layoutResId: Int,
initAdapter: SimpleRecyclerView.Adapter.() -> Unit
) {
this.adapter = SimpleRecyclerView.Adapter().apply {
initAdapter()
viewHolder(layoutResId)
}
}

fun SimpleRecyclerView.Adapter.viewHolder(
@LayoutRes layoutResId: Int
) {
makeViewHolder = { parent: ViewGroup, _: Int ->
VH(parent.context.inflate(layoutResId, parent))
}
}

fun SimpleRecyclerView.Adapter.onBind(
onBind: (viewHolder: RecyclerView.ViewHolder, position: Int) -> Unit
) {
onBindListener = onBind
}

fun Context.inflate(@LayoutRes layoutResId: Int, parent: ViewGroup, attachToRoot: Boolean = false): View =
LayoutInflater.from(this).inflate(layoutResId, parent, attachToRoot)


こんな感じで関数の引数に

f: Adapter.()->Unit

みたいなラムダ式の定義のレシーバー(上のAdapterの部分)がわかれば、こんな感じのものを作るのに迷うことが減るとおもいます。

さあ、これにあとはレイアウトファイルを用意してあげて、画面遷移の実装をするだけで簡単なものは作れるでしょう!


もっとよいライブラリあれば教えてください

僕がしらないだけかもしれないので、こんな感じにレイアウトファイルさえつくっておけばサクッとRecyclerViewを作れるみたいなライブラリがあればおしえてもらえたら嬉しいですー。ちょっと似ていて、マージンとか設定するやつはあるんですけど、いやそれやないねん・・・みたいなものが多いので。

あと面白そうなカスタマイズアイデアや、ツッコミがあれば気兼ねなくコメントしていってくださいね。みんなで気楽にAndroidアプリつくれるようにしましょう!!!

以上。駄コード失礼いたしました。