#8 Kotlin で TODO アプリを開発・リリースするまでの記録

# はじめに
[前回の記事](https://qiita.com/m_n_r/items/710fb8d0d851899f1457)から時を超えました。
あまりにもきりが悪いのがずっと気にかかっていたので、舞い戻りました。

# 前回のあらすじ
### 前回までの記事
[#1 Kotlin で TODO アプリを開発・リリースするまでの記録](https://qiita.com/m_n_r/items/03ac9f641f735ad40d43)
[#2 Kotlin で TODO アプリを開発・リリースするまでの記録](https://qiita.com/m_n_r/items/90f9f96bf1a8b4f91613)
[#3 Kotlin で TODO アプリを開発・リリースするまでの記録](https://qiita.com/m_n_r/items/d3f15df9b0f4974b3dc5)
[#4 Kotlin で TODO アプリを開発・リリースするまでの記録](https://qiita.com/m_n_r/items/31bff60b2c867ecfc73d)
[#5 Kotlin で TODO アプリを開発・リリースするまでの記録](https://qiita.com/m_n_r/items/c65838ed7ac8a2a4442a)
[#6 Kotlin で TODO アプリを開発・リリースするまでの記録](https://qiita.com/m_n_r/items/5a4729eff728175c84c2)
[#7 Kotlin で TODO アプリを開発・リリースするまでの記録](https://qiita.com/m_n_r/items/710fb8d0d851899f1457)

### 前回のあらすじ
- リストに削除ボタンを設置した（イメージ）

### 今回の目標
- RecyclerView のデータ追加・削除

# Android Studio のショートカット設定

本題に入る前に、我慢ならなかったのでショートカットを変更しました。
**Ctrl + Y（やり直す）したら、行が削除される悲劇を二度と繰り返さないように。**

[EclipseとAndroid Studioのショートカットの違い for Windows](https://qiita.com/decchi/items/f8603ccccec03a71a4d9)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477634/6ce6f2d3-c94e-c7c8-da09-d040fc71134f.png)

複数種類があるので、慣れたショートカットを選択できそうです。
著者は Visual Studio にしました。

# Android Studio 3.5
[「Android Studio 3.5」が正式リリース ～「Chrome OS 75」以降を新たに公式サポート](https://forest.watch.impress.co.jp/docs/news/1202489.html)

アップデートしちゃいました。
本記事以降はこちらを適用した前提で進めていきます。

# RecyclerView のデータ追加・削除
それでは前回に引き続き、こちらの記事をまねしてリスト表示してみます。

**[RecyclerViewと仲良くなる](http://bigbuddha.hatenablog.jp/entry/recyclerview-beginner)**

## Adapter

データの追加メソッドを記載します。

```kotlin:RecyclerAdapter.kt
// リストにデータを追加する
fun addListItem (item: Item) {
    itemList.add(item)
    notifyDataSetChanged() // これを忘れるとRecyclerViewにItemが反映されない
}
```

```kotlin:RecyclerAdapter.kt
// リストのデータを削除する
private fun removeItem(position: Int) {
    itemList.removeAt(position)
    notifyItemRemoved(position)
    notifyDataSetChanged() // これを忘れるとRecyclerViewにItemが反映されない
}
```

データ削除のメソッドが private になっているのは
おそらくこのメソッド内で完結できる処理だからでしょう。

## データクラス

データクラスを定義します。
Item.kt ファイルを MainActivity,kt と同じ階層に新規作成して記述していきます。

```kotlin:Item.kt
import java.util.*

data class Item(
    val mId: String = UUID.randomUUID().toString(),
    val mName: String
)
```

ひとまずは識別子(mId)と TODO のテキスト(mName)を用意しました。

### データクラスってなに？
> [はじめてのKotlin:データクラス～プロパティ、コンストラクタ、ゲッター不要～](https://qiita.com/teruroom/items/4366658d0dc9ab860918)
> kotlin(コトリン)にはデータクラスというものがあります。これは処理は行わないけれどもデータだけをもつクラスです。

これにデータを持たせているようです。
一時的なデータ保管だと思われるので
今後データベースに登録する処理も必要となってくると思いますが
ひとまず登録ができる状態にしたいですね。

## Adapter
順番が前後しましたが、Adapter の onCreateViewHolder と onBindViewHolder も
修正していきます。

それぞれのメソッドの意味はこちらの記事をご覧ください。

[今更だけど、RecyclerViewの基本をまとめてみた](https://qiita.com/ujikawa1026/items/562185be74cc4bf6f7de#5-adapter%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E4%BD%9C%E6%88%90%E3%81%97%E3%81%A6%E3%83%87%E3%83%BC%E3%82%BF%E3%81%A8view%E3%81%AE%E7%B4%90%E4%BB%98%E3%81%91%E3%82%92%E8%A1%8C%E3%81%86)

```kotlin:RecyclerAdapter.kt
// ViewHolderを作成
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {

    val layoutInflater = LayoutInflater.from(context)
    val mView = layoutInflater.inflate(R.layout.list_item, parent, false)

    // リストのクリックイベントを作成
    mView.setOnClickListener { view ->
        mRecyclerView?.let {
            itemClickListener.onItemClick(view, it.getChildAdapterPosition(view))
        }
    }

    return RecyclerViewHolder(mView)
}
```

修正します、と言いつつも onCreateViewHolder は何も変わっていません。
リストのクリックイベントは前の処理の名残で、無くても問題ないです。

```kotlin:RecyclerAdapter.kt
// ViewHolderにデータをセット
override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
    // itemList.mNameをリスト上のテキストボックスにセット
    holder.itemTextView.text = itemList[position].mName
    // itemList.itemImageButtonにリムーブメソッドをセット
    holder.itemImageButton.setOnClickListener {
        removeItem(position)
    }
}
```

こちらは全体的に修正しました。

## activity_main.xml
現在は list_item.xml にテキストの表示と、削除ボタンを設置している状態です。
追加ボタンが置けないので、activity_main.xml をサンプルに沿った構成に変更します。

``` kotlin:activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <LinearLayout
                android:id="@+id/container"
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:padding="8dp">

                <LinearLayout
                        android:orientation="horizontal"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content" >
            
                        <android.support.design.widget.TextInputLayout
                                android:layout_width="0dp"
                                android:layout_height="wrap_content"
                                android:layout_weight="4" >
                
                                <EditText
                                        android:id="@+id/et_item"
                                        android:layout_height="match_parent"
                                        android:layout_width="match_parent"
                                        android:hint="Item Name"
                                        android:maxLines="1"
                                        android:inputType="text" />
                
                        </android.support.design.widget.TextInputLayout>
                
                        <ImageButton
                                android:id="@+id/bt_add_item"
                                android:layout_width="0dp"
                                android:layout_height="match_parent"
                                android:layout_weight="1"
                                android:src="@drawable/ic_add_black_24dp"
                                android:background="#000f"
                                android:contentDescription="Add" />
                </LinearLayout>
            
                <androidx.recyclerview.widget.RecyclerView
                        android:id="@+id/mainRecyclerView"
                        android:layout_width="match_parent"
                        android:layout_height="0dp"
                        android:layout_weight="9"
                        android:scrollbars="vertical">
                </androidx.recyclerview.widget.RecyclerView>
        </LinearLayout>

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

ほとんどコピペなので、例の如く Warning が表示されました。

### Warning の解消
#### ImageButton

```
Hardcoded text
```

要するに `android:contentDescription="Add"` に対して
「Add」とベタ書きはやらない方がいいよ、という警告です。

> [【Android】Hardcoded string “＊＊＊”, should use @ string resource.] (http://buravo46.hatenablog.com/entry/2015/02/18/153805)
> 表示する文字列の設定ファイルがきちんと用意されているらしく、そのファイルに文字を記述して> 画面の設定ファイルなどで使うのが推奨されているみたいです。

上記の通り、文字列の設定ファイルに表示する文字を用意します。

``` kotlin:strings.xml
<resources>
    <string name="app_name">first</string> // 最初から設定されている
    <string name="itemImageButton">AddButton</string> // 新たに追加
</resources>
```

strings.xml で設定した文字列を取得するように書き換え、Warning が解消されました。

``` kotlin:activity_main.xml
<ImageButton
        android:id="@+id/bt_add_item"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:src="@drawable/ic_add_black_24dp"
        android:background="#000f"
        android:contentDescription="@string/itemImageButton" />
```
#### EditText

```
Use Autofill
```

EditText では次のプロパティを設定しておかないと Warnig が表示されるようなので
hint の内容を含め書き換えを行います。

> [[Android] スマホにEditTextを使って文字を入力する](https://akira-watson.com/android/edittext.html)
> android:hint
> android:autofillHints
> android:inputType
> を設定しないとwarningが出ます。

``` kotlin:activity_main.xml
<EditText
        android:id="@+id/et_item"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:hint="@string/hint"
        android:autofillHints="@string/hint"
        android:maxLines="1"
        android:inputType="text" />
```

@string/hint は前項にも出てきた strings.xml に追加します。

``` kotlin:strings.xml
<resources>
    <string name="app_name">first</string>
    <string name="itemImageButton">AddButton</string>
    <string name="hint">input</string> // 新たに追加
</resources>
```

そのほか、LinearLayout を二重に指定しないとエラーが出たので注意してください。

## MainActivity.kt

RecyclerView がリスト表示する記述は残し
EditText に入力された値を登録する処理を追加します。
具体的には bt_add_item のクリックイベントがそれに該当します。


``` kotlin:MainActivity.kt
class MainActivity : AppCompatActivity(), RecyclerViewHolder.ItemClickListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mainRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)

        // Initialize Adapter
        val adapter = RecyclerAdapter(this, this, ArrayList())

        bt_add_item.setOnClickListener {
            if ( !TextUtils.isEmpty( et_item.text.toString() ) ) {
                val task = Item( mName = et_item.text.toString() )
                adapter.addListItem(task)
            } else {
                Snackbar.make(container, "Task is empty", Snackbar.LENGTH_SHORT).show();
            }
            et_item.setText("")
        }

        mainRecyclerView.adapter = adapter

        // 区切り線の表示
        mainRecyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
    }

    override fun onItemClick(view: View, position: Int) {
        Toast.makeText(applicationContext, "position $position was tapped", Toast.LENGTH_SHORT).show()
    }
}
```

## ビルド実行

エラーが出ました。
うまくいかなくても落ち込まないことが大事。

### エラーを分析する

4: Run タブに大量のエラーが出ているので、最初の 3 行を切り出して原因を探っていきます。

```
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.first, PID: 9873
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.first/com.example.first.MainActivity}: android.view.InflateException: Binary XML file line #22: Binary XML file line #22: Error inflating class android.support.design.widget.TextInputLayout
```

`android.support.design.widget.TextInputLayout` がエラーの原因の様子。

いろいろ試してみましたが改善せず、ひとまず`android.support.design.widget.TextInputLayout` をコメントアウトしたところ起動成功しました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477634/37c7b666-136e-a4f6-32e4-b9abfb9e3ff8.png" width=30%>
TODO アプリでもリストでもなんでもなくて、ちょっとおもしろい。

このタグが悪いのはわかるのですが、対処方法がわからないため
Design タブ > Palette > Text > TextInputLayout を
Compornent Tree の該当位置にドラッグアンドドロップで配置させます。
（もとの TextInputLayout はコメントアウトしているため 1 つだけになっています）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477634/dac34ebd-b890-a372-0ac5-d350d9baaf6e.png)
Text タブで配置されたタグを確認します。

``` kotlin:activity_main.xml
<com.google.android.material.textfield.TextInputLayout
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1">

    <com.google.android.material.textfield.TextInputEditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="hint" />
</com.google.android.material.textfield.TextInputLayout>
```

あっ！

```
<android.support.design.widget.TextInputLayout
↓
<com.google.android.material.textfield.TextInputLayout

<EditText
↓
<com.google.android.material.textfield.TextInputEditText
```

コピペはやめようってこと。**コピペはやめようってこと！**

``` kotlin:activity_main.xml
<com.google.android.material.textfield.TextInputLayout
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_weight="4">

         <com.google.android.material.textfield.TextInputEditText
                 android:id="@+id/et_item"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:hint="@string/hint"
                 android:inputType="text"
                 android:maxLines="1" />

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

自分の環境に対して、適切なコンポーネントを使用することが大事。
ということで、コンポーネント名と属性を追加・修正し直しました。
前述にあった [EditText](#EditText) は使用しないため `android:autofillHints` は記述していません。

それから `android.support.design.widget.TextInputLayout` を使用しないということは
[#7 Android Support Design Library](https://qiita.com/m_n_r/items/710fb8d0d851899f1457#android-support-design-library) で「エラー出てるけど動くからいいか」問題も解消されます。

以下の記述を build.gradle から削除しました。

``` kotlin:[app]build.gradle
implementation 'com.android.support:design:28.0.0' 
```

もとから記載している Google Material が代わりを担ってくれます。
よかったよかった。

``` kotlin:[app]build.gradle
implementation 'com.google.android.material:material:1.0.0'
```

### エラーの解消

#### container(LinearLayout)
どのタイミングからエラーが発生していたのか気づかなかったのですが
container(LinearLayout) でエラーが発生しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477634/dac34ebd-b890-a372-0ac5-d350d9baaf6e.png)

エラー内容はこちら。

```
Missing Constraints in ContrainLayout
```

> [Android StudioでConstraintLayoutが自動補完されずにエラーが出る問題の解決](https://asmsuechan.hatenablog.com/entry/2018/12/05/213205)
> 対象オブジェクトを選択して、上のボタン群の中にある「Infer Constraints」をクリックしましょう。

Design タブで container を選択状態で「Infer Constraints」をクリックします。
すぐにエラーが解消されたので、Text タブで補完内容を確認しました。

``` kotlin:activity_main.xml
<LinearLayout
        android:id="@+id/container"
        android:layout_width="405dp"
        android:layout_height="733dp"
        android:orientation="vertical"
        android:padding="8dp"
        tools:layout_editor_absoluteX="6dp">
↓
<LinearLayout
        android:id="@+id/container"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:orientation="vertical"
        android:padding="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">
```

全体的なサイズを親サイズに合わせて調整してくれました。

> [Androidの新しいLayout、ConstraintLayoutことはじめ](https://qiita.com/tomoima525/items/0584be581a0d3a4db8c5)
> ConstraintLayoutは自動的にレイアウトの位置をマテリアルデザインに沿った最適な位置に調整してくれます

マテリアルデザインに沿って自動的にやってくれるようです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477634/a73f6add-a515-0900-614c-46d9b7680dc3.png" width=30%>

テキスト入力のボックス表示ができました。

# リストにデータを表示する
テキストを追加し、リストにデータが反映されるか確認します。

#### テキスト入力
テキストボックスに入力します。
パソコンのキーボードでもエミュレーターのキーボードでも入力可能です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477634/f4e59836-4a59-b606-9e57-5f30543a3451.png" width=30%>

#### 追加ボタンでリスト表示(３件)
テキストを入力し「＋」の追加ボタンでデータを登録します。
「red」「blue」「yellow」の順に行いましたが、上から順に登録・表示されています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477634/f7e1d85f-df5d-5ad0-65ef-af079055b02e.png" width=30%>

#### 削除ボタンでデータ削除
リスト中央の「blue」を「－」の削除ボタンで削除しました。
画面上からきちんと消えています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477634/250dd381-d5e2-dfdb-fba0-574ba7a82bf2.png" width=30%>

#### 追加ボタンでさらに追加
テキストを入力し「＋」の追加ボタンで「green」を登録します。
正しく「red」「yellow」「green」の順で登録・表示されています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477634/123baf40-294c-7bcb-67c6-f9d92ff75563.png" width=30%>

成功です！

# まとめ
長かった。空いた時間のほうが長かった。
しばらく振りに [#6](https://qiita.com/m_n_r/items/5a4729eff728175c84c2) を見返して何がしたかったのかを思い出してみると
どうやら、状態を保存する配列を欲していたようですね。
今回データクラスというものがデータを保存してくれているので
これを活用して、幅を広げることができそうです。

### 成果
- リストデータの表示ができた
- リストデータの登録ができた
- リストデータの削除ができた

### 参考記事に感謝
たくさんの記事を引用させて頂きました。ありがとうございます。
著者が一部のみを引用したことにより、引用元の意図しない情報として受けられることがないよう、できれば引用元の記事を読んでいただけますようお願いいたします。
またいずれの記事の感想も、著者個人の意見に基づくものですのでご注意ください。

本記事で参考にさせて頂いたこちらの記事に、改めて特別な感謝を。

**[RecyclerViewと仲良くなる](http://bigbuddha.hatenablog.jp/entry/recyclerview-beginner)**
# 次の記事

\#9 Kotlin で TODO アプリを開発・リリースするまでの記録