【Android】Kotlin + Room + RecyclerViewを使ってメモアプリを作る

# はじめに
RoomとRecyclerViewを使った簡単なメモアプリを作成したので共有します。
RoomはSQLiteを活用したローカルデータベースであり、公式でも推奨されています。
参考：[Room を使用してローカル データベースにデータを保存する](https://developer.android.com/training/data-storage/room?hl=ja)

今回はメモの追加と編集、削除ができる簡単なアプリを作成しました。またAdapterにはListAdapterを採用しています。
ソースコードは[こちら](https://github.com/noodle2290/RoomMemo)から
![gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/962236/9e873889-7dd0-e0e9-49da-137e3cdf92a4.gif)



# 環境
+ Kotlin 1.6.10
+ Android Studio Bumblebee | 2021.1.1
+ Room 2.2.2
+ targetSDK: 32
# 概要
1. Roomの初期設定
2. View Bindingの設定
3. Entityの作成
4. Daoの作成
4. RoomDatabaseの作成
4. ListAdapterの作成
4. Mainactivityで一覧表示
4. EditActivityでメモの追加と編集、削除

## 1. Roomの初期設定
### app/build.gradle の dependencies に以下を追加
```kotlin:app/build.gradle
    def room_version = "2.2.2"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
```
### app/build.gradle の plugins に以下を追加
```kotlin:app/build.gradle
    id "org.jetbrains.kotlin.kapt"
```
Bumblubeeからライブラリの導入の仕方が変わったので注意しましょう。


## 2. View Bindingの設定
### build.gradleのandroidに以下のコードを追加します
```kotlin:build.gradle
    buildFeatures{
        viewBinding true
    }
```
### MainActivity.ktに以下のコードを追加します
```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater).apply { setContentView(this.root) }
    }
}
```
これでView Bindingの設定は終わりです。

## 3. Entityの作成
### data class Memo を作成しEntityを定義します
```kotlin:Memo.kt
@Entity(tableName = "memos")
data class Memo(
    @PrimaryKey(autoGenerate = true) val id: Int,
    val content: String?
)
```
ここでデータベースのテーブルEntity を定義しています。idをPrimaryKeyとして設定し、また自動生成されるようにautoGenerrateをtrueにしています。

## 4. Daoの作成
### interface MemoDao.ktを作成しDaoを定義します

```kotlin:MemoDao.kt
@Dao
interface MemoDao {
    @Insert
    fun insert(memo : Memo)

    @Update
    fun update(memo : Memo)

    @Delete
    fun delete(memo : Memo)

    @Query("delete from memos")
    fun deleteAll()

    @Query("select * from memos")
    fun getAll(): List<Memo>

    @Query("select * from memos where id = :id")
    fun getMemo(id: Int): Memo
}
```
データベースにアクセスするためのインターフェイスMemoDaoを定義します。ここで後に使うメソッドを定義します。
## 5. RoomDatabaseの作成
### MemoDatabase.ktを作成しDatabaseを定義します
```kotlin:MemoDatabase.kt
@Database(entities = [Memo::class], version = 1)
abstract class MemoDatabase : RoomDatabase() {
    abstract fun memoDao(): MemoDao
}
```
## 6. ListAdapterの作成
### memo_itemの作成
```xml:memo_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/memo_text_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:text="Hello World!"
        android:textColor="#000000"
        android:textSize="24sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.divider.MaterialDivider
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        app:dividerInsetEnd="16dp"
        app:dividerInsetStart="16dp"
        app:layout_constraintTop_toBottomOf="@+id/memo_text_view"
        tools:layout_editor_absoluteX="0dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
```
RecyclerViewに表示するmemo_itemを作成します。
### MemoAdapterの作成
```kotlin:MemoAdapter.kt
class MemoAdapter(private val onClickListener: OnClickListener): ListAdapter<Memo, MemoViewHolder>(diffUtilItemCallback) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MemoViewHolder {
        val view = MemoItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return MemoViewHolder(view)
    }

    override fun onBindViewHolder(holder: MemoViewHolder, position: Int) {
        val memo = getItem(position)
        holder.bind(memo)
        holder.itemView.setOnClickListener {
            onClickListener.onClick(memo)
        }
    }
}

class MemoViewHolder(
    private val binding: MemoItemBinding
) : RecyclerView.ViewHolder(binding.root) {
    fun bind(memo: Memo) {
        binding.memoTextView.text = memo.content
    }
}

private val diffUtilItemCallback = object : DiffUtil.ItemCallback<Memo>() {
    override fun areContentsTheSame(oldItem: Memo, newItem: Memo): Boolean {
        return oldItem == newItem
    }

    override fun areItemsTheSame(oldItem: Memo, newItem: Memo): Boolean {
        return oldItem.id == newItem.id
    }
}

class OnClickListener(val clickListener: (memo: Memo) -> Unit) {
    fun onClick(memo: Memo) = clickListener(memo)
}
```
RecyclerViewAdapterではなくListAdapterを使用しています。

## 7. MainActivityで一覧表示
### MainActivityを変更していきます
```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </androidx.recyclerview.widget.RecyclerView>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:srcCompat="@android:drawable/ic_menu_add" />
</androidx.constraintlayout.widget.ConstraintLayout>
```
```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater).apply { setContentView(this.root) }
        //EditActivityは後で作成します
        val toEditIntent = Intent(this,EditActivity::class.java)

        //roomのインスタンス生成
        val db = Room.databaseBuilder(
            applicationContext,
            MemoDatabase::class.java, "database-name"
        ).allowMainThreadQueries().build()

        //Daoのインスタンス生成
        val memoDao = db.memoDao()
        //roomのデータを全て取得
        val memos: List<Memo> = memoDao.getAll()
        //OnClickListenerを引数としてMemoAdapterのインスタンス生成
        val memoAdapter = MemoAdapter(
            OnClickListener { memo ->
                toEditIntent.putExtra("ID",memo.id)
                startActivity(toEditIntent)
            }
        )
        memoAdapter.submitList(memos)
        binding.recyclerView.adapter = memoAdapter
        binding.recyclerView.layoutManager =
            LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)

        binding.fab.setOnClickListener {
            startActivity(toEditIntent)
        }
    }
}
```
## 7. EditActivityでメモの追加と編集、削除
```xml:activity_edit
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".EditActivity">

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/edit_text"
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:passwordToggleEnabled="false">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/text_input"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="メモを入力"
            android:imeOptions="actionDone"
            android:maxLines="1"
            android:textSize="14sp" />

    </com.google.android.material.textfield.TextInputLayout>

    <Button
        android:id="@+id/save_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="28dp"
        android:layout_marginEnd="24dp"
        android:text="登録"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/edit_text" />

    <Button
        android:id="@+id/delete_button"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="28dp"
        android:layout_marginEnd="16dp"
        android:text="削除"
        app:layout_constraintEnd_toStartOf="@+id/submit_button"
        app:layout_constraintTop_toBottomOf="@+id/edit_text" />

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

```kotlin:EditActivity.kt
private lateinit var binding: ActivityEditBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityEditBinding.inflate(layoutInflater).apply { setContentView(this.root) }

        val db = Room.databaseBuilder(
            applicationContext,
            MemoDatabase::class.java, "database-name"
        ).allowMainThreadQueries().build()

        val id:Int = intent.getIntExtra("ID",0)
        val memoDao = db.memoDao()
        val memo = memoDao.getMemo(id)
        val toMainIntent = Intent(this, MainActivity::class.java)

        if(id == 0) {
            binding.deleteButton.isInvisible = true
            binding.saveButton.setOnClickListener {
                val memo = Memo(
                    id,
                    binding.textInput.text.toString()
                )
                memoDao.insert(memo)
                startActivity(toMainIntent)
            }
        }else{
            binding.deleteButton.isVisible = true
            binding.textInput.setText(memo.content)
            binding.saveButton.setOnClickListener {
                val memo = Memo(
                    id,
                    binding.textInput.text.toString()
                )
                memoDao.update(memo) 
                startActivity(toMainIntent)
            }
            binding.deleteButton.setOnClickListener {
                memoDao.delete(memo)
                startActivity(toMainIntent)
            }
        }
    }
```
# 参考
[【Android】Kotlin + Firestore + RecyclerViewを使ってTodoアプリを作る](https://qiita.com/Kaito-Dogi/items/b2da06db10a95fef84b3)
[【Android】Room を使ったサンプルと解説](https://qiita.com/kaleidot725/items/34f29efaeb6d836e010e)