KotlinでToDoリストアプリを作成 - その３

## はじめに
前回はRoomライブラリを使ってデータを永続化しました。今回はDate APIやDatePickerDialog、DialogFragmentを使用し、UIの拡充を行いました。
[KotlinでToDoリストアプリを作成 - その１](https://qiita.com/ist-h-i/items/b243a0bf0bef45fa2b7e)
[KotlinでToDoリストアプリを作成 - その２](https://qiita.com/ist-h-i/items/df611e0655301258191b)

## 概要
KotlinでToDoリストアプリを作成します。

:::note warn
プロジェクト全体のソースコードはこちらに置いてあります。
https://github.com/ist-h-i/ToDoApp.git
※JavaやGradleバージョンのアップデートやKapt（Kotlin Annotation Processing Tool）からKSP（Kotlin Symbol Processing）への変更をしております。
:::

### 今回実装する機能
- リストのソート機能
- 編集機能
- 表示要素の追加
- カレンダー画面の追加

### 実装に用いる技術
- Date API
- Duration（日付ベースの時間の量「23.4秒」などを表します）
- Period（日付ベースの時間の量「2年3か月と4日」などを表します）
- LocalDate（タイムゾーンのないローカル日付）
- DateTimeFormatter（日付/時間オブジェクトの出力および解析のためのフォーマッタ）
- DatePickerDialog
- DialogFragment
- Kotlinx Coroutines（非同期実行するコードを簡略化できる並行実行のデザイン パターン）

## 実装
### MainActivity.kt (メインクラス)
```kotlin
package com.example.mytodo

import android.annotation.SuppressLint
import android.app.DatePickerDialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.room.Room
import com.example.mytodo.model.Priority
import com.example.mytodo.room.Todo
import com.example.mytodo.room.TodoDAO
import com.example.mytodo.room.TodoDatabase
import com.example.mytodo.ui.DatePick
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDate
import java.time.Period
import java.time.format.DateTimeFormatter

class MainActivity : AppCompatActivity(), DatePickerDialog.OnDateSetListener {

    // ToDo追加用の画面を用意（未初期化）
    private lateinit var addTodoView: View

    // Room Databaseを用意（未初期化）
    private lateinit var db: TodoDatabase
    private lateinit var dao: TodoDAO

    // 表示するリストを用意（今は空）
    private var addList = ArrayList<Todo>()

    // ソート用のフラグを用意
    private var sortedByDeadline = false
    private var sortedByPriority = false

    // RecyclerViewを宣言
    private lateinit var recyclerView: RecyclerView

    // RecyclerViewのAdapterを用意（未初期化）
    private lateinit var recyclerAdapter: RecyclerAdapter

    // 期限日の優先度のための数値
    private var deadlineNum: Int = 0

    @OptIn(DelicateCoroutinesApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // ヘッダータイトルを非表示
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE)

        // Viewをセット
        setContentView(R.layout.activity_main)

        // View要素を取得
        val btnAdd: Button = findViewById(R.id.btnAdd)
        recyclerView = findViewById(R.id.rv)

        // コンテンツを変更してもRecyclerViewのレイアウトサイズを変更しない場合はこの設定を使用してパフォーマンスを向上
        recyclerView.setHasFixedSize(true)

        // レイアウトマネージャーで列数を2列に指定
        recyclerView.layoutManager = GridLayoutManager(this, 2, RecyclerView.VERTICAL, false)
        val itemDecoration: RecyclerView.ItemDecoration =
            DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
        recyclerView.addItemDecoration(itemDecoration)

        // Roomオブジェクトを初期化
        db = Room.databaseBuilder(
            this, TodoDatabase::class.java, "todo.db"
        ).build()
        dao = db.todoDAO()

        // 初期表示時にToDoレコードを全件取得
        GlobalScope.launch {
            // RoomからTodoリストを取得
            addList = dao.getAll() as ArrayList<Todo>

            // recyclerAdapterの初期化
            recyclerAdapter = RecyclerAdapter(addList)

            recyclerAdapter.setOnCellClickListener(
                // ToDoカードビューのクリック処理
                object : RecyclerAdapter.OnCellClickListener {
                    override fun onItemClick(position: Int) {
                        generateAlertDialog(true, position)
                    }
                })
            // RecyclerViewにAdapterをセット
            recyclerView.adapter = recyclerAdapter
        }

        // 追加ボタン押下時にAlertDialogを表示する
        btnAdd.setOnClickListener {
            generateAlertDialog(false, 0)
        }

        // 表示しているアイテムがタッチされた時の設定
        val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
            // アイテムをドラッグできる方向を指定
            ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT,
            // アイテムをスワイプできる方向を指定
            ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
        ) {
            // アイテムドラッグ時の挙動を設定
            override fun onMove(
                recyclerView: RecyclerView,
                viewHolder: RecyclerView.ViewHolder,
                target: RecyclerView.ViewHolder,
            ): Boolean {
                // アイテム位置の入れ替えを行う
                val fromPos = viewHolder.adapterPosition
                val toPos = target.adapterPosition
                recyclerAdapter.notifyItemMoved(fromPos, toPos)
                // Roomの情報を更新
                val fromData = addList[fromPos]
                fromData.displayOrder = toPos
                val toData = addList[toPos]
                toData.displayOrder = fromPos
                GlobalScope.launch {
                    dao.upsert(fromData)
                    dao.upsert(toData)
                }
                return true
            }

            // アイテムスワイプ時の挙動を設定
            @SuppressLint("NotifyDataSetChanged")
            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                // アイテムスワイプ時にAlertDialogを表示
                android.app.AlertDialog.Builder(this@MainActivity)
                    // AlertDialogのタイトルを設定
                    .setTitle(R.string.removeTitle)
                    // AlertDialogのyesボタンを設定
                    .setPositiveButton(R.string.yes) { arg0: DialogInterface, _: Int ->
                        try {
                            // AlertDialogを非表示
                            arg0.dismiss()
                            // Roomから削除
                            val todo = addList[viewHolder.adapterPosition]
                            val id =
                                todo.title.hashCode() / 4 - todo.detail.hashCode() / 4 + todo.deadline.hashCode() / 4 - todo.priority.hashCode() / 4
                            GlobalScope.launch {
                                dao.delete(id)
                            }
                            // UIスレッドで実行
                            runOnUiThread {
                                // スワイプされたアイテムを削除
                                addList.removeAt(viewHolder.adapterPosition)
                                // 表示するリストを更新(アイテムが削除されたことを通知)
                                recyclerAdapter.notifyItemRemoved(viewHolder.adapterPosition)
                            }
                        } catch (ignored: Exception) {
                        }
                    }.setNegativeButton(R.string.no) { _: DialogInterface, _: Int ->
                        // 表示するリストを更新(アイテムが変更されたことを通知)
                        recyclerAdapter.notifyDataSetChanged()
                    }
                    // AlertDialogを表示
                    .show()
            }
        })

        // 表示しているアイテムがタッチされた時の設定をリストに適用
        itemTouchHelper.attachToRecyclerView(recyclerView)
    }

    private fun generateAlertDialog(isUpdate: Boolean, index: Int) {
        val nullParent: ViewGroup? = null
        addTodoView = layoutInflater.inflate(R.layout.add_todo, nullParent)
        // AlertDialog内の表示項目を取得
        val txtTitle: EditText = addTodoView.findViewById(R.id.title)
        val txtDetail: EditText = addTodoView.findViewById(R.id.detail)
        val btnDeadline: Button = addTodoView.findViewById(R.id.deadline)
        val priorityButtonGroup: RadioGroup = addTodoView.findViewById(R.id.radioGroup)
        if (isUpdate) {
            // 編集時
            txtTitle.setText(addList[index].title)
            txtDetail.setText(addList[index].detail)
            btnDeadline.text = addList[index].deadline
            when (addList[index].priority) {
                Priority.IMPORTANT -> {
                    priorityButtonGroup.check(R.id.RadioButtonB1)
                }

                Priority.NORMAL -> {
                    priorityButtonGroup.check(R.id.RadioButtonB2)
                }

                Priority.UNIMPORTANT -> {
                    priorityButtonGroup.check(R.id.RadioButtonB3)
                }
            }
        } else {
            // 新規登録時
            priorityButtonGroup.check(R.id.RadioButtonB3)
        }
        // AlertDialogを生成
        android.app.AlertDialog.Builder(this)
            // AlertDialogのタイトルを設定
            .setTitle(R.string.addTitle)
            // AlertDialogの表示項目を設定
            .setView(addTodoView)
            // AlertDialogのyesボタンを設定し、押下時の挙動を記述
            .setPositiveButton(R.string.yes) { _: DialogInterface, _: Int ->
                if (isUpdate) {
                    // 編集時
                    val id = addList[index].id
                    val displayOrder = addList[index].displayOrder
                    // 入力内容を取得
                    val title = txtTitle.text.toString()
                    val detail = txtDetail.text.toString()
                    val deadline = btnDeadline.text.toString()
                    val deadlineNum = if (deadline == R.string.deadline.toString()) Int.MAX_VALUE else deadlineNum
                    val priority = when (priorityButtonGroup.checkedRadioButtonId) {
                        R.id.RadioButtonB1 -> Priority.IMPORTANT
                        R.id.RadioButtonB2 -> Priority.NORMAL
                        R.id.RadioButtonB3 -> Priority.UNIMPORTANT
                        else -> Priority.IMPORTANT
                    }
                    updateTodoData(
                        Todo(
                            id, displayOrder, title, detail, deadline, deadlineNum, priority
                        ), index
                    )
                } else {
                    // 新規登録時
                    // 入力内容を取得
                    val title = txtTitle.text.toString()
                    val detail = txtDetail.text.toString()
                    val deadline = btnDeadline.text.toString()
                    val deadlineNum = if (deadline == R.string.deadline.toString()) Int.MAX_VALUE else deadlineNum
                    val priority = when (priorityButtonGroup.checkedRadioButtonId) {
                        R.id.RadioButtonB1 -> Priority.IMPORTANT
                        R.id.RadioButtonB2 -> Priority.NORMAL
                        R.id.RadioButtonB3 -> Priority.UNIMPORTANT
                        else -> Priority.IMPORTANT
                    }
                    val id =
                        title.hashCode() / 4 - detail.hashCode() / 4 + deadline.hashCode() / 4 - priority.hashCode() / 4
                    // ToDoを生成
                    insertTodoData(
                        Todo(
                            id, addList.size, title, detail, deadline, deadlineNum, priority
                        )
                    )
                }
            }
            // AlertDialogのnoボタンを設定
            .setNegativeButton(R.string.no, null)
            // AlertDialogを表示
            .show()
    }

    @OptIn(DelicateCoroutinesApi::class)
    private fun insertTodoData(data: Todo) {
        // Todoアイテムの重複をチェック
        if (addList.stream().anyMatch { e ->
                e.title == data.title && e.detail == data.detail && e.deadline == data.deadline && e.priority == data.priority
            }) {
            Toast.makeText(
                this, R.string.duplicateTitle, Toast.LENGTH_SHORT
            ).show()
            return
        }
        // 表示するリストの最後尾に追加
        addList.add(data)
        // 表示するリストを更新(アイテムが挿入されたことを通知)
        recyclerAdapter.notifyItemInserted(addList.size - 1)
        GlobalScope.launch {
            // Roomに追加
            dao.upsert(data)
        }
    }

    @OptIn(DelicateCoroutinesApi::class)
    private fun updateTodoData(data: Todo, index: Int) {
        // Todoアイテムの重複をチェック
        if (addList.indexOf(data) != index && addList.stream().anyMatch { e ->
                e.title == data.title && e.detail == data.detail && e.deadline == data.deadline && e.priority == data.priority
            }) {
            Toast.makeText(
                this, R.string.duplicateTitle, Toast.LENGTH_SHORT
            ).show()
            return
        }
        // 表示するリストの更新
        addList[index] = data
        // 表示するリストを更新(アイテムが更新されたことを通知)
        recyclerAdapter.notifyItemChanged(index)
        GlobalScope.launch {
            // Roomに追加
            dao.upsert(data)
        }
    }

    override fun onDateSet(view: DatePicker, year: Int, monthOfYear: Int, dayOfMonth: Int) {
        // カレンダー入力時
        val dateFormat = DateTimeFormatter.ofPattern("uu/MM/dd(E)")
        val deadline: LocalDate = LocalDate.of(year, monthOfYear + 1, dayOfMonth)
        val period: Period = Period.between(LocalDate.now(), deadline)
        val duration: Duration = Duration.between(
            LocalDate.now().atTime(0, 0, 0), deadline.atTime(0, 0, 0)
        )
        val remainYears = if (period.years != 0) period.years + R.string.yearUnit else ""
        val remainMonths = if (period.months != 0) period.months + R.string.monthUnit else ""
        val txtDeadline =
            if (period.isNegative) R.string.expired else dateFormat.format(deadline) + "\n" + R.string.remain + remainYears + remainMonths + period.days + R.string.dayUnit
        addTodoView.findViewById<Button>(R.id.deadline).text = txtDeadline.toString()
        deadlineNum = duration.toDays().toInt()
    }

    fun showDatePickerDialog(@Suppress("UNUSED_PARAMETER") v: View) {
        // カレンダー表示
        val newFragment = DatePick()
        newFragment.show(supportFragmentManager, "datePicker")
    }

    @SuppressLint("NotifyDataSetChanged")
    fun orderByPriority(@Suppress("UNUSED_PARAMETER") v: View) {
        // 優先度でソート
        sortedByPriority = !sortedByPriority
        if (addList.isEmpty()) {
            Toast.makeText(
                this, R.string.emptyTodoList, Toast.LENGTH_SHORT
            ).show()
        }
        addList.sortWith(compareBy<Todo> { it.priority }.thenBy { it.displayOrder })
        if (sortedByPriority) {
            addList.reverse()
        }
        // 表示するリストを更新(アイテムが変更されたことを通知)
        recyclerAdapter.notifyDataSetChanged()
    }

    @SuppressLint("NotifyDataSetChanged")
    fun orderByDeadline(@Suppress("UNUSED_PARAMETER") v: View) {
        // 期限日でソート
        sortedByDeadline = !sortedByDeadline
        if (addList.isEmpty()) {
            Toast.makeText(
                this, R.string.emptyTodoList, Toast.LENGTH_SHORT
            ).show()
        }
        addList.sortWith(compareBy<Todo> { it.deadlineNum }.thenBy { it.displayOrder })
        if (sortedByDeadline) {
            addList.reverse()
        }
        // 表示するリストを更新(アイテムが変更されたことを通知)
        recyclerAdapter.notifyDataSetChanged()
    }

}
```

### RecyclerAdapter.kt (アイテムを生成し、リストに適用するクラス)
```kotlin
package com.example.mytodo

import android.graphics.Color
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView
import com.example.mytodo.model.Priority
import com.example.mytodo.room.Todo

class RecyclerAdapter(private val todoList: ArrayList<Todo>) :
    RecyclerView.Adapter<RecyclerAdapter.ViewHolderItem>() {

    // リスナを格納する変数を定義（未初期化）
    private lateinit var listener: OnCellClickListener

    // インターフェースを作成
    interface OnCellClickListener {
        fun onItemClick(position: Int)
    }

    // リスナーをセット
    fun setOnCellClickListener(clickListener: OnCellClickListener) {
        listener = clickListener
    }

    // リストに表示するアイテムの表示内容
    inner class ViewHolderItem(v: View) : RecyclerView.ViewHolder(v) {
        val titleHolder: TextView = v.findViewById(R.id.title)
        val detailHolder: TextView = v.findViewById(R.id.detail)
        val deadlineHolder: TextView = v.findViewById(R.id.deadline)
        var cardView: CardView = v.findViewById(R.id.cardView)
    }

    // リストに表示するアイテムを生成
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderItem {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.one_layout, parent, false)
        return ViewHolderItem(view)
    }

    // position番目のデータを表示
    override fun onBindViewHolder(holder: ViewHolderItem, position: Int) {
        val currentItem = todoList[position]
        holder.titleHolder.text = currentItem.title
        holder.detailHolder.text = currentItem.detail
        holder.deadlineHolder.text = currentItem.deadline
        if (currentItem.deadline == "期限切れ") {
            holder.deadlineHolder.setTextColor(Color.RED)
            holder.deadlineHolder.typeface = Typeface.DEFAULT_BOLD
        } else {
            holder.deadlineHolder.setTextColor(Color.GRAY)
            holder.deadlineHolder.typeface = Typeface.DEFAULT
        }
        val frameColor = when (currentItem.priority) {
            Priority.IMPORTANT -> Color.parseColor("#FF4444")
            Priority.NORMAL -> Color.parseColor("#FFBB33")
            else -> Color.parseColor("#33B5E5")
        }
        holder.cardView.setCardBackgroundColor(frameColor)
        holder.cardView.setOnClickListener {
            listener.onItemClick(position)
        }
    }

    // リストサイズを取得する用のメソッド
    override fun getItemCount(): Int {
        return todoList.size
    }
}
```

### DatePick.kt (カレンダー生成)
```kotlin
package com.example.mytodo.ui

import android.app.DatePickerDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.example.mytodo.MainActivity
import java.time.LocalDateTime

class DatePick : DialogFragment(), DatePickerDialog.OnDateSetListener {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val today: LocalDateTime = LocalDateTime.now()
        val year = today.year
        val month = today.monthValue - 1
        val day = today.dayOfMonth

        return DatePickerDialog(
            this.context as Context, activity as MainActivity?, year, month, day
        )
    }

    override fun onDateSet(
        view: android.widget.DatePicker, year: Int, monthOfYear: Int, dayOfMonth: Int
    ) {
    }
}
```

### Priority.kt (ToDoの優先度をEnumで定義)
```kotlin
package com.example.mytodo.model

enum class Priority(val value: Int) {
    IMPORTANT(1),
    NORMAL(2),
    UNIMPORTANT(3);

    companion object {
        fun fromInt(value: Int) = Priority.values().first { it.value == value }
    }
}
```

### one_layout.xml (リストに表示する内容)
```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:cardBackgroundColor="@android:color/holo_blue_light"
    app:cardCornerRadius="6dp"
    app:cardElevation="6dp"
    app:cardUseCompatPadding="true"
    app:contentPadding="8dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:orientation="vertical"
        android:padding="5dp">

        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:lines="2"
            android:textColor="@android:color/black"
            android:textSize="15sp"
            android:textStyle="bold" />

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="?android:attr/listDivider" />

        <TextView
            android:id="@+id/detail"
            android:layout_width="match_parent"
            android:layout_height="80dp"
            android:lines="5"
            android:textColor="@android:color/darker_gray"
            android:textSize="13sp" />

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="?android:attr/listDivider" />

        <TextView
            android:id="@+id/deadline"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:lines="2"
            android:text="@string/deadline"
            android:textColor="@android:color/darker_gray"
            android:textSize="11sp" />

    </LinearLayout>
</androidx.cardview.widget.CardView>
```

### add_todo.xml (ToDo追加ボタン押下時に表示するビューの内容)
```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="wrap_content">

    <EditText
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:autofillHints="auto"
        android:hint="@string/title"
        android:inputType="textMultiLine"
        android:textSize="20sp"
        app:layout_constraintBottom_toTopOf="@id/detail"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="VisualLintTextFieldSize" />

    <EditText
        android:id="@+id/detail"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:autofillHints="auto"
        android:breakStrategy="simple"
        android:hint="@string/detail"
        android:inputType="textMultiLine"
        android:textSize="20sp"
        app:layout_constraintBottom_toTopOf="@+id/attribute"
        app:layout_constraintTop_toBottomOf="@id/title" />

    <LinearLayout
        android:id="@+id/attribute"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        app:layout_constraintBottom_toTopOf="@id/deadline"
        app:layout_constraintTop_toBottomOf="@id/detail">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="10dp"
            android:text="@string/priority"
            android:textSize="15sp" />

        <RadioGroup
            android:id="@+id/radioGroup"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <RadioButton
                android:id="@+id/RadioButtonB1"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:background="@android:color/holo_red_light"
                android:gravity="center"
                android:text="@string/high"
                android:textSize="15sp" />

            <RadioButton
                android:id="@+id/RadioButtonB2"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:background="@android:color/holo_orange_light"
                android:gravity="center"
                android:text="@string/middle"
                android:textSize="15sp" />

            <RadioButton
                android:id="@+id/RadioButtonB3"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:background="@android:color/holo_blue_light"
                android:gravity="center"
                android:text="@string/low"
                android:textSize="15sp" />
        </RadioGroup>
    </LinearLayout>

    <Button
        android:id="@+id/deadline"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="5pt"
        android:hint="@string/deadline"
        android:onClick="showDatePickerDialog"
        android:textColor="@color/white"
        android:textColorHint="@color/white"
        android:textSize="15sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/attribute" />

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

### main.xml
```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/rv"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_margin="0dp"
        app:layout_constraintBottom_toTopOf="@id/buttonArea"
        app:layout_constraintTop_toTopOf="parent" />

    <LinearLayout
        android:id="@+id/buttonArea"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="0dp"
        android:gravity="end"
        app:layout_constraintBottom_toBottomOf="parent">

        <Button
            android:id="@+id/btnOrderPriority"
            style="?android:attr/buttonBarButtonStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="10dp"
            android:layout_marginBottom="10dp"
            android:backgroundTint="@color/purple_200"
            android:onClick="orderByPriority"
            android:text="@string/priorityOrder"
            android:textSize="15sp" />

        <Button
            android:id="@+id/btnOrderDeadline"
            style="?android:attr/buttonBarButtonStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="10dp"
            android:layout_marginBottom="10dp"
            android:backgroundTint="@color/purple_200"
            android:onClick="orderByDeadline"
            android:text="@string/deadlineOrder"
            android:textSize="15sp" />

        <Button
            android:id="@+id/btnAdd"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="10dp"
            android:layout_marginBottom="10dp"
            android:text="@string/addButton"
            android:textSize="15sp"
            tools:ignore="ButtonStyle" />
    </LinearLayout>

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

## プロジェクトビルド&実行
ヘッダーメニューよりBuild > Rebuild Projectを行いエラーが出ないことを確認して実行

初期表示（データ0件）
![1-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/531e4425-41f7-4e16-aec1-b3982c6c9047.jpeg)

ToDo１件目追加（優先度：高 / 期日：未来日）
![2-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/73e5a001-c1eb-9622-40cd-a28a00050de2.jpeg) ![2-2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/eae3ad73-c984-2f4f-921d-54e7f65c0222.jpeg) ![2-3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/9d8faa38-be09-7318-c840-c76c6fb61d62.jpeg) ![3-4.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/354053ae-b790-2283-b12f-9dba51a4a21b.jpeg) ![2-5.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/05ecae5f-227c-0698-26dd-2c85cc8d7729.jpeg)

ToDo２件目追加（優先度：中 / 期日：過去日）
![3-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/6207bf1c-e015-731a-25dc-1099c230aa78.jpeg) ![3-2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/01f1be45-9e02-21f6-0c3c-f4650d3104d5.jpeg) ![3-3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/4587d9fe-570b-d037-5b6a-959f9f10ad63.jpeg) 

ToDo３件目追加（優先度：低 / 期日：未来日）
![3-4.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/0faba680-bdb3-3b7c-b20f-59d0b9e3190d.jpeg) ![3-5.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/42d5c6b3-9722-6cc6-0eed-341fbb56fddd.jpeg)

優先度順でソート（昇順 ⇔ 降順）
![4-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/b6fdb163-eae5-9fbb-a324-b7480492d29a.jpeg) ![4-2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/948968eb-26ca-df2f-d9e2-05e2627ef982.jpeg)

期日順でソート（昇順 ⇔ 降順）
![5-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/3bb9f747-2a4e-7c76-a74b-7802eb5d1bcd.jpeg) ![5-2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/4028cf5a-cccf-1bea-f3fc-60f80e96185a.jpeg)

## まとめ
今回はUIの拡充を行いました。Date APIやDialogFragmentは使用機会が多いので使い慣れておきたいですね。
次回はこのアプリにAWS DynomoDBとの接続やFragmentを使った画面遷移などを追加して、タスク管理アプリを作成する予定です。