Android Jetpack（Room + Compose）でTodoアプリ作ろう【後編】

みなさんこんにちは！この記事ではTodoアプリの続きをRoomを使ってデータベースにデータを保存する部分の実装を行いたいと思います。
前編をご覧になってない方はそちらを先にご覧ください。

https://qiita.com/MasashiHamaguchi/items/6e96703cb8b512c41aac

今回作成したTodoアプリのソースコードはこちらにあります。（GitHub）

https://github.com/masashihamaguchi/TodoApp-Jetpack


### 完成イメージ

<img alt="device-2022-05-15-165226.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245180/fbb843e3-41f3-9dec-1e97-f682176eea3e.gif" width=300>

# 実装手順

## 1. Roomを導入する

`build.gradle`の`dependencies`に下の2行を追加します。

```groovy:build.gradle
dependencies {

    // for Room
    implementation "androidx.room:room-runtime:2.4.2"
    kapt "androidx.room:room-compiler:2.4.2"

    ....
}
```

app/build.gradleの`plugins`にkotlin-kapt`を追加します。

```groovy:build.gradle:app
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt' // 追加
}
```


追加できたら、`Sync Now`を押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245180/a1fffc46-5dfc-a738-0546-12f7b0415538.png)

## 2. Entityを定義する

データベースのテーブルにあたるEntityを定義します。
Data Classに`@Entity`とアノテーションをつけるだけで簡単に定義することができます。

新しくTodoクラス用のファイルを生成して、Entityを定義します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245180/a88d2e90-dec8-2873-831a-cd288f401f0a.png)

```kotlin:Todo.kt
@Entity(tableName = "todos")
data class Todo(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val title: String = "",
    val created_at: Date = Date()
)
```

## 3. Daoを定義する

```kotlin:TodoDao.kt
@Dao
interface TodoDao {
    @Query("select * from todos order by created_at asc")
    fun getAll(): MutableList<Todo>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun post(todo: Todo)

    @Delete
    fun delete(todo: Todo)
}
```

## 4. DateTimeConverterクラスを作成する

RoomではDate型を扱えないため、Long型にキャストするコンバーターを作成します。

```kotlin:DateTimeConverter.kt
class DateTimeConverter {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time?.toLong()
    }
}
```

## 5. RoomDatabaseを実装する
```kotlin:AppDatabase.kt
@Database(entities = [Todo::class], version = 1, exportSchema = false)
@TypeConverters(DateTimeConverter::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun todoDao(): TodoDao
}
```

## 6. RoomApplicationクラスを作成する
`Application`クラスを継承した`RoomApplication`クラスを作成します。

```kotlin:RoomApplication.kt
class RoomApplication : Application() {
    companion object {
        lateinit var database: AppDatabase
    }

    override fun onCreate() {
        super.onCreate()

        database = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            "todos"
        ).build()
    }
}
```

マニフェストファイルに作成したRoomApplicationクラスを指定します。

```xml:AndroidManifest.xml
    <application
        android:name=".RoomApplication"
        
        .... >
```

## 7. MainActivityを書き換える

MainActivityを順番に書き換えていきます。

### Composable関数を書き換える

MainScreenをMainActivityの中に移動します。

```kotlin:MainActivity.kt
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ....
    }

    @Composable
    fun MainScreen() {
        ....
    }
}
```

MainScreenの中の`todoList`変数をを削除して`text`変数だけにします。
また引数に`todoList`を追加します。

MainScreenの中を以下のように書き換えます。

```kotlin:MainActivity.kt
    @Composable
    fun MainScreen(todoList: SnapshotStateList<Todo>) {
        var text: String by remember { mutableStateOf("") }

        Column {
            TopAppBar(
                title = { Text("Todo List") }
            )
            LazyColumn(
                modifier = Modifier.fillMaxWidth().weight(1f)
            ) {
                items(todoList) { todo ->
                    key(todo.id) {
                        TodoItem(todo)
                    }
                }
            }
            Row(
                modifier = Modifier.fillMaxWidth().padding(16.dp)
            ) {
                OutlinedTextField(
                    value = text,
                    onValueChange = { it -> text = it },
                    label = { Text("ToDo") },
                    modifier = Modifier.wrapContentHeight().weight(1f)
                )
                Spacer(Modifier.size(16.dp))
                Button(
                    onClick = {
                        if (text.isEmpty()) return@Button
                        postTodo(text)
                        text = ""
                    },
                    modifier = Modifier.align(Alignment.CenterVertically)
                ) {
                    Text("ADD")
                }
            }

        }
    }
```

`MainScreen`の下に`TodoItem`を追加します。

```kotlin:MainActivity.kt
    @Composable
    fun TodoItem(todo: Todo) {
        val sdf = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")

        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
                .clickable(onClick = { deleteTodo(todo) })
        ) {
            Text(
                text = todo.title,
                modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)
            )
            Text(
                text = "created at: ${sdf.format(todo.created_at)}",
                fontSize = 12.sp,
                color = Color.LightGray,
                textAlign = TextAlign.Right,
                modifier = Modifier.fillMaxWidth()
            )
        }
    }
```

### データベースを操作する関数を作成する

Todoを読み込み、作成、削除するための関数を作成する。
データベースの操作はメインスレッドで行えないため、Coroutineを使って実装します。

`MainActivity`の直下にデータベースを操作するための`TodoDao`クラスのインスタンスとTodoを管理するリストを作成します。
onCreateの下に`loadTodo`、`postTodo`、`deleteTodo`の３つの関数を作成します。

```kotlin:MainActivity.kt
class MainActivity : ComponentActivity() {

    private val dao = RoomApplication.database.todoDao()
    private var todoList = mutableStateListOf<Todo>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        ....
    }

    private fun loadTodo() {
        CoroutineScope(Dispatchers.Main).launch {
            withContext(Dispatchers.Default) {
                dao.getAll().forEach { todo ->
                    todoList.add(todo)
                }
            }
        }
    }

    private fun postTodo(title: String) {
        CoroutineScope(Dispatchers.Main).launch {
            withContext(Dispatchers.Default) {
                dao.post(Todo(title = title))

                todoList.clear()
                loadTodo()
            }
        }
    }

    private fun deleteTodo(todo: Todo) {
        CoroutineScope(Dispatchers.Main).launch {
            withContext(Dispatchers.Default) {
                dao.delete(todo)

                todoList.clear()
                loadTodo()
            }
        }
    }

    ....
}
```

### `onCreate`を書き換える

`onCreate`を以下のように書き換えます。

```kotlin:MainActivity.kt
class MainActivity : ComponentActivity() {

    private val dao = RoomApplication.database.todoDao()
    private var todoList = mutableStateListOf<Todo>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TodoAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    MainScreen(todoList)
                }
            }
        }

        loadTodo()
    }

    ....
}
```

アプリを実行してみましょう。
タスクを作成・削除できることを確認してください。

<img alt="スクリーンショット 2022-05-15 15.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245180/dc447639-c1e7-9dd0-43de-fcf2e60c9d18.png" width=300>


# 完成

ここまで実装できれば完成です！（下の動画ではアイコンも表示しています。）
テキストフィールドから新しくタスクを追加することができ、タスクをタップすると削除することができます。

アプリを閉じてもデータは保存されています。

<img alt="device-2022-05-15-165226.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245180/02161e28-b7c2-f188-a6bf-72e2f4ae7249.gif" width=300>


# 参考資料

https://matsudamper.hatenablog.com/entry/2021/07/05/192057

https://qiita.com/kaleidot725/items/34f29efaeb6d836e010e

https://qiita.com/tkt989/items/d600cf995a5ea41598f6