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

## はじめに
Android初学者向けにKotlinでToDoリストアプリを作成してみました。30分程度でアプリ立ち上げができるのでぜひお試しください。

## 概要
Android Studio(Electric Eel)でToDoリストアプリを作成します。

### 今回実装する機能
* ToDoリスト表示機能
* ToDo追加機能
* ToDo移動機能
* ToDo削除機能

### 実装に用いる技術
* RecyclerViewを用いてリスト一覧を作成
* CardViewを用いてToDoを作成
* GridLayoutを用いてリストを編集
* AlertDialogを用いてメッセージ表示


## 導入
Android Studioのインストール&セットアップ
https://developer.android.com/studio/install?hl=ja

### プロジェクト作成
Android StudioのヘッダーメニューよりFile > New > New Project…
![studio1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/2b3a9e61-5f16-5e5b-f414-60d7ba681f1a.png)


Empty Activityを選択しNext
![studio2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/dedb6fc8-a3a8-3639-7ee7-ad98c8e36da9.png)


任意のNameを入力しLangageをKotlinのままFinish
![studio3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/4447fe08-a8f5-3f5c-00a1-f986f1011639.png)


## 実装
### activity_main.xml
①activity_main.xmlを開き、②Splitを選択、③必要のない`<TextView ~/>`を削除
![xml1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/70032aa2-323e-a976-0595-7dbdce4871cf.png)


①Parletteから検索を行い、ButtonとRecyclerViewを画面にドラッグ&ドロップ
②idを設定し、文字やレイアウトを調節
![xml2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/aef9b4b7-3bef-b49e-dc90-db83da723c06.png)


android:text="+"上で option+Enter(Mac) / Alt+Enter(Windows) を押下し、Extract string resourceから登録を行う
![xml3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/ea494322-53df-fb34-db7b-d40d267f4dc6.png)


```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">

    <Button
        android:id="@+id/btnAdd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:text="@string/addButton"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

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

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

### 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_marginStart="20dp"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="20dp"
        android:autofillHints="auto"
        android:hint="@string/title"
        android:inputType="text"
        android:textSize="20sp"
        app:layout_constraintBottom_toTopOf="@id/detail"
        app:layout_constraintTop_toBottomOf="parent"
        tools:ignore="VisualLintTextFieldSize" />

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

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

### 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:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:cardCornerRadius="6dp"
    app:cardElevation="6dp"
    app:cardUseCompatPadding="true"
    app:contentPadding="8dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

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

        <View
            android:id="@+id/divider"
            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="65dp"
            android:maxLines="4"
            android:textColor="@android:color/darker_gray"
            android:textSize="13sp" />

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

### Todo.kt (リストに表示するアイテム)
```
package com.example.mytodo

data class Todo (
    val title: String,
    val detail: String
)
```

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

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import java.util.*

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

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

    // リストに表示するアイテムを生成
    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
    }

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

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

import android.content.DialogInterface
import android.os.Bundle
import android.view.Window
import android.widget.Button
import android.widget.EditText
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


class MainActivity : AppCompatActivity() {

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

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

    // RecyclerViewのAdapterを用意
    private var recyclerAdapter = RecyclerAdapter(addList)

    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)

        // RecyclerViewにAdapterをセット
        recyclerView.adapter = recyclerAdapter

        // 追加ボタン押下時にAlertDialogを表示する
        btnAdd.setOnClickListener {

            // AlertDialog内の表示項目を取得
            val view = layoutInflater.inflate(R.layout.add_todo, null)
            val txtTitle: EditText = view.findViewById(R.id.title)
            val txtDetail: EditText = view.findViewById(R.id.detail)

            // AlertDialogを生成
            android.app.AlertDialog.Builder(this)
                // AlertDialogのタイトルを設定
                .setTitle(R.string.addTitle)
                // AlertDialogの表示項目を設定
                .setView(view)
                // AlertDialogのyesボタンを設定し、押下時の挙動を記述
                .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int ->
                    // ToDoを生成
                    val data = Todo(txtTitle.text.toString(), txtDetail.text.toString())
                    // 表示するリストの最後尾に追加
                    addList.add(data)
                    // 表示するリストを更新(アイテムが挿入されたことを通知)
                    recyclerAdapter.notifyItemInserted(addList.size - 1)
                }
                // AlertDialogのnoボタンを設定
                .setNegativeButton(R.string.no, null)
                // AlertDialogを表示
                .show()
        }

        // 表示しているアイテムがタッチされた時の設定
        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)
                    return true
                }

                // アイテムスワイプ時の挙動を設定
                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()
                                // 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)
    }
}
```

## プロジェクトビルド&実行
①ヘッダーメニューよりBuild > Rebuild Projectを行いエラーが出ないことを確認して②実行
![run.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/92ef9f86-b8c9-48c2-0608-62ecaed993b8.png)


### 初期表示
![init.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/00131f83-810e-207a-0197-3b8739000827.jpeg)


### ToDo追加
![add1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/86081fc0-a028-aee6-3fae-742db5b30484.jpeg) ![add2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/a0fd6836-67fd-e096-a0cd-902ee4deaa83.jpeg) ![add3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/36ed3c96-d29a-ed2d-7509-a53b6f9e4406.jpeg) ![add4.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/18264aa5-6e60-7ef2-3eb5-452aad90ec42.jpeg)


### ToDo移動
![move1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/563b74b2-8fc9-c510-af23-f8c980e90a9b.jpeg) ![move2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/761b7f0b-7424-c387-3417-9b0414a0b3d5.jpeg)
 

### ToDo削除
![delete1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/0919c495-fa29-131d-89be-c8fc564f3d56.jpeg) ![delete2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/c2df58b6-c37f-74d2-209d-44a3f5599b27.jpeg) 


## まとめ
今回はシンプルなアプリを作成してみました。アプリを立ち上げるたびにリセットされるため永続化を行う必要があったり、期日や優先度などToDoの内容を充実させ検索機能を追加したり、ログイン機能を追加したりとまだまだエンハンスしていく必要があります。
とはいえ自作アプリケーションの作成の一歩目の参考になれば幸いです。
次回、上記のエンハンス対応を行う予定です。