LoginSignup
0
8

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-01-12

モチベーション

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アプリつくれるようにしましょう!!!

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

0
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
8