KotlinでAndroidアプリを開発（AnkoでSQLiteと一覧表示編）

# 検証環境
この記事の内容は、以下の環境で検証しました。

* Java:open jdk 1.8.0_152
* Android Studio 3.0 Beta 2
* CompileSdkVersion:26
* MinSdkVersion:19
* TargetSdkVersion:26
* BuildToolsVersion:26.0.1
* Anko SQL:0.10.1

# 目標
以下の内容を理解する

* KotlinとAnkoでSQLiteを扱う方法
* AnkoでSQLiteのinsert
* AnkoでSQLiteのselect

# アプリの内容
下記の記事で作成したアプリに改良を加える

https://qiita.com/naoi/items/5cdaa72e4d8903b6a36e

今回は以下の機能を追加

* 計算したBMIとその時の年月日時をDBに登録
* 登録したBMIの一覧表示

完成イメージは以下の通り。
![完成イメージ.png](https://qiita-image-store.s3.amazonaws.com/0/99960/41edae55-9709-912c-2c5c-1ac3bd8024cd.png)


# 実装内容

## パッケージ情報

| パッケージ名|格納ファイル|
|:-----------:|:----:|
|jp.co.casareal.sample.bmisample|Activityを格納|
|jp.co.casareal.sample.bmisample.entity|データクラスを格納|
|jp.co.casareal.sample.bmisample.adapter|ListViewに表示する時のAdapterを格納|
|jp.co.casareal.sample.bmisample.bo|ビジネスロジック類を格納|
|jp.co.casareal.sample.bmisample.db|SQLiteOpenHelperを継承したクラスを格納|

## 作成したファイル一覧
### 共通

|ファイル名|概要|
|:-----------:|:-----------------------------:|
|strings.xml|文字列の定義ファイル|
|jp.co.casareal.sample.bmisample.entity.PersonalData|身長と体重を格納するデータクラス|
|jp.co.casareal.sample.bmisample.entity.ListData|DB一行分のデータを格納するクラス|

### データベース
|ファイル名|概要|
|:-----------:|:-----------------------------:|
|jp.co.casareal.sample.bmisample.db.BMIDatabaseOpenHelper|テーブルの作成やデータベース接続を行うクラス|
|jp.co.casareal.sample.bmisample.bo.ListDataParser|DBから取得した１行分を解析するクラス|

### アダプター
|ファイル名|概要|
|:-----------:|:-----------------------------:|
|jp.co.casareal.sample.bmisample.adapter.BMIListAdapter|ListViewのカスタムアダプター|


### 入力画面
|ファイル名|概要|
|:-----------:|:-----------------------------:|
|jp.co.casareal.sample.bmisample.MainActivity|入力画面のActivity。<br>ボタンが押された時の処理<br>登録されたBMIの一覧表示画面に遷移するボタンの処理|
|layout/activity_main.xml|入力画面のレイアウトを定義したファイル|

### 結果画面
|ファイル名|概要|
|:-----------:|:-----------------------------:|
|jp.co.casareal.sample.bmisample.ResultActivity|結果画面のActivity<br>計算したBMIを登録するボタンの処理|
|layout/activity_result.xml|結果画面のレイアウトを定義したファイル|

### 一覧画面
|ファイル名|概要|
|:-----------:|:-----------------------------:|
|jp.co.casareal.sample.bmisample.BMIListActivity|一覧画面のActivity<br>DBに登録されているデータを表示|
|layout/activity_bmilist.xml|一覧画面のレイアウトを定義したファイル|
|layout/row.xml|ListViewに表示する一行分のレイアウトファイル|


## コードと説明
前回の記事で説明した部分は省略する。
（前回の記事：http://qiita.com/naoi/items/5cdaa72e4d8903b6a36e）
差分のみを説明する。

### gradle
今回はAnkoを利用するのでモジュールのgradleに以下を追記
AnkoはJetBrains公式のライブラリ
様々な便利な機能が入っている
今回はSQLiteに関する機能を使用する

```groovy:
dependencies {
　　・・・省略・・・
    compile 'org.jetbrains.anko:anko:0.10.1'
    compile "org.jetbrains.anko:anko-sqlite:0.10.1"
}
```

### 共通
#### データクラス
データベースの１行分に該当するデータクラス
一覧表示にも使用する

```kotlin:ListData
package jp.co.casareal.sample.bmisample.entity

data class ListData(var bmi:String,var date:String)
```

#### データベース
##### BMIDatabaseOpenHelper
データベース・アクセスに必要となるクラス
処理としては、自身のオブジェクトをSingletonとしている。
また、テーブルの作成をしている。

今回作成するデータベースは以下の通り。
![er.png](https://qiita-image-store.s3.amazonaws.com/0/99960/c2d59144-6642-5bb5-b7cc-cd036923edb0.png)


```kotlin:BMIDatabaseOpenHelper.kt
package jp.co.casareal.sample.bmisample.db

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import org.jetbrains.anko.db.ManagedSQLiteOpenHelper
import org.jetbrains.anko.db.TEXT
import org.jetbrains.anko.db.createTable

class BMIDatabaseOpenHelper (context:Context):ManagedSQLiteOpenHelper(context,"bmidb.db",null,1)
{
    companion object {
        val tableName = "bmi"
        private  var instance :BMIDatabaseOpenHelper? = null;

        fun getInstance(context: Context):BMIDatabaseOpenHelper{
            return instance ?: BMIDatabaseOpenHelper(context.applicationContext)!!
        }
    }
    override fun onCreate(db: SQLiteDatabase?) {
        db?.run { createTable(tableName,ifNotExists = true,
            columns = *arrayOf( "date" to TEXT, "bmi" to TEXT))
        }
     }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
    }


}
```

Ankoでは既存のSQLiteOpenHelperクラスをシームレスに置き換えられるクラス、**ManagedSQLiteOpenHelper**クラスを用意している。
その為、**ManagedSQLiteOpenHelper**クラスのコンストラクタには、SQLiteOpenHelperクラスと同様に以下の順で値を渡している。

```kotlin
class BMIDatabaseOpenHelper (context:Context):ManagedSQLiteOpenHelper(context,"bmidb.db",null,1)
```

|順番|説明|
|:--|:--|
|第１引数：Context|言わずと知れたContextクラス|
|第２引数：String|データベースファイル名|
|第３引数：CursorFactory|カスタマイズしたCursorを使用したい時に使用する。今回はnull|
|第４引数：Int|データベースファイルのバージョン|

このクラスのオブジェクトをキャッシュするために**Companion Object**を使用している
Javaで言うところのstaticメソッドやstatic変数に該当する

tableNameという変数名にテーブル名を定義し、static変数のように扱えるようにした。
instanceという変数名にこのクラスのオブジェクトを格納させ、Singletonの作りにしている。

```kotlin
    companion object {
        val tableName = "bmi"
        private  var instance :BMIDatabaseOpenHelper? = null;

        fun getInstance(context: Context):BMIDatabaseOpenHelper{
            return instance ?: BMIDatabaseOpenHelper(context.applicationContext)!!
        }
    }
```
また、ここでは「?:」演算子を使用している。**エルビス演算子(elvis operator)**と呼ばれている。
instance変数がnullでなければ、instance変数のオブジェクトを返し、nullの場合はオブジェクトを生成して返している。

```kotlin
return instance ?: BMIDatabaseOpenHelper(context.applicationContext)!!
```

onCreateメソッドではテーブルの作成を行っている。
ankoではテーブル作成のメソッドを用意しており。より簡単にテーブルが作成出来るようになっている。

```kotlin
    override fun onCreate(db: SQLiteDatabase?) {
        db?.run { createTable(tableName,ifNotExists = true,
            columns = *arrayOf( "date" to TEXT, "bmi" to TEXT))
        }
     }
```

テーブル作成では、SQLiteDatabaseのオブジェクトを使用する。その時に**run**拡張関数を使用している。
nullでなければ、SQLiteDatabaseクラスのcreateTableメソッドで、テーブルを作成するようにしている。

```kotlin
 db?.run { 
 }
```

テーブル作成ではcreateTableメソッドを使用している。
カラム名と型を指定している箇所で使用している**arrayOf**にアスタリスクを指定している。これはvarargで値を渡す時に事前に配列を作成したばあいは、その変数やarrayOfにアスタリスクが必要となる。

```kotlin
createTable(tableName,ifNotExists = true,
            columns = *arrayOf( "date" to TEXT, "bmi" to TEXT))
```
引数と意味は以下の通り。

|順番|説明|
|:--|:--|
|第１引数：String|テーブル名|
|第２引数：Boolean|ifNotExistsの有効無効をbooleanで指定する|
|第３引数：vararg columns: Pair<String, SqlType>)|カラム名と型を指定する。<br>型名は|org.jetbrains.anko.db.SqlTypeの定数を指定|

今回は使用しないため、onUpgradeメソッドはから実装とした。

```kotlin
    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
    }
```

##### ListDataParser 
データベースで検索した結果の１行分をデータクラスに格納するためのクラス。
使用場面は後述とするが、引数で受け取ったColumnsに対し、カラム名を指定してデータを取得する。

```kotlin:ListDataParser 
package jp.co.casareal.sample.bmisample.bo

import jp.co.casareal.sample.bmisample.entity.ListData
import org.jetbrains.anko.db.MapRowParser

class ListDataParser : MapRowParser<ListData> {
    override fun parseRow(columns: Map<String, Any?>): ListData {
        return ListData(columns["bmi"] as String, columns["date"] as String)
    }
}
```

パースするクラスを生成するには**MapRowParser**クラスか**RowParser**を継承する
ジェネリクスでパース後のクラスの型を指定する

```kotlin
class ListDataParser : MapRowParser<ListData>
```

引数のcolumnsには検索結果が格納されている。
map型であるため、カラム名を指定して取得する。
また、Anyとしてとれるため、データクラスのプロパティに格納するときは型変換が必要となる。

```kotlin
    override fun parseRow(columns: Map<String, Any?>): ListData {
        return ListData(columns["bmi"] as String, columns["date"] as String)
    }
```

#### アダプター
##### ListViewで表示する１行分のレイアウト

```xml:row.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:text="@string/label_result_date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/rowDate"
            android:layout_weight="1.0"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:text="@string/label_result_bmi"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/rowBmi"
            android:layout_weight="1.0"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
</LinearLayout>
```

##### 1行分のViewを生成して表示する処理

既存のAdapterと同じ使用方法をとっている。
しかし、Kotlinで記述することによって簡潔に記述可能。
特に、getViewメソッドの第２引数のnullチェックはエルビス演算子を使用することにより完結になっている。
また、表示するデータを取得したときのnullチェックも**let**を使用するこにより、より簡潔に記述できる。

```kotlin:BMIListAdapter.kt
package jp.co.casareal.sample.bmisample.adapter

import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import jp.co.casareal.sample.bmisample.R
import jp.co.casareal.sample.bmisample.entity.ListData
import org.jetbrains.anko.layoutInflater

class BMIListAdapter : ArrayAdapter<ListData> {

    constructor(context: Context?, resource: Int) : super(context, resource)
    constructor(context: Context?, resource: Int, textViewResourceId: Int) : super(context, resource, textViewResourceId)
    constructor(context: Context?, resource: Int, objects: Array<out ListData>?) : super(context, resource, objects)
    constructor(context: Context?, resource: Int, textViewResourceId: Int, objects: Array<out ListData>?) : super(context, resource, textViewResourceId, objects)
    constructor(context: Context?, resource: Int, objects: MutableList<ListData>?) : super(context, resource, objects)
    constructor(context: Context?, resource: Int, textViewResourceId: Int, objects: MutableList<ListData>?) : super(context, resource, textViewResourceId, objects)

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {

        var newView = convertView ?: context.layoutInflater.inflate(R.layout.row, null)

        getItem(position)?.run {
            newView.findViewById<TextView>(R.id.rowBmi).text = bmi
            newView.findViewById<TextView>(R.id.rowDate).text = date
        }
        return newView
    }
}
```

### 結果画面
#### 計算したBMIをデータベースに登録する処理を追加

レイアウトファイルにボタンを追加している。
※前回との差分のみ掲載。

```xml:activity_result.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="jp.co.casareal.sample.bmisample.ResultActivity">

・・・省略・・・

    <Button
        android:id="@+id/btnRegisterDB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/btn_register_database"
        android:layout_marginTop="8dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="8dp"
        app:layout_constraintStart_toStartOf="@+id/textView4"
        android:layout_marginStart="8dp"
        app:layout_constraintEnd_toEndOf="@+id/kindTextView"
        android:layout_marginEnd="8dp"
        app:layout_constraintHorizontal_bias="0.511"
        app:layout_constraintVertical_bias="0.378" />
</android.support.constraint.ConstraintLayout>

```

ボタンが押された時の処理のなかで、DBに接続して登録する処理を記述した。
※前回との差分のみ掲載。

```kotlin:ResultActivity.kt
package jp.co.casareal.sample.bmisample

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import jp.co.casareal.sample.bmisample.db.BMIDatabaseOpenHelper
import jp.co.casareal.sample.bmisample.entity.PersonalData
import kotlinx.android.synthetic.main.activity_result.*
import org.jetbrains.anko.db.insert
import java.text.SimpleDateFormat

class ResultActivity : AppCompatActivity() {

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

    ・・・省略・・・

        btnRegisterDB.setOnClickListener{
            var helper = BMIDatabaseOpenHelper.getInstance(this);
            helper.use {
                insert(BMIDatabaseOpenHelper.tableName,*arrayOf(
                        "bmi" to bmiTextView.text,"date" to format.format(System.currentTimeMillis())
                ))
            }
        }
    }
}

```

作成しておいた**BMIDatabaseOpenHelper**クラスのオブジェクトを取得する。
Companionを使用して、staticメソッドみたいにアクセスするには「クラス名.メソッド名」で呼び出す。

```kotlin
var helper = BMIDatabaseOpenHelper.getInstance(this);
```

データベースにデータを登録するときBMIDatabaseOpenHelperクラスのオブジェクトを使用する。データベースに接続した場合、closeメソッドを呼び出す事が必要となる。
ここで有用なのが**use**となる。
useを使用することにより、Closeable インタフェースを実装したクラスのcloseメソッドを処理終了後に、closeを半強制的にしてくれる。そのため、ここではuseを使用している。

```kotlin
helper.use {
 ・・・省略・・・
}
```

insertメソッドを使用することでデータベースに登録ができる。
また、登録するデータはto関数でカラム名と登録するデータとしておくことが可能。to関数を使用するとtoでつなげた２つの値で、Pairを生成してくれる。

引数は２つで第１引数にはテーブル名をしていする。第２引数に登録するデータを指定する。

```kotlin
insert(BMIDatabaseOpenHelper.tableName,*arrayOf(
                        "bmi" to bmiTextView.text,"date" to format.format(System.currentTimeMillis())
))
```

### 入力画面
#### 一覧画面に遷移する処理の追加

一覧画面に遷移するボタンと処理を追加
※差分のみを記載

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="jp.co.casareal.sample.bmisample.MainActivity"
    tools:layout_editor_absoluteX="0dp"
    tools:layout_editor_absoluteY="81dp">


   ・・・省略・・・

    <Button
        android:id="@+id/btnBMIList"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/btn_bmi_list"
        android:layout_marginTop="8dp"
        app:layout_constraintTop_toBottomOf="@+id/button"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginEnd="8dp"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_marginStart="8dp"
        android:onClick="onClickBMIList"/>

</android.support.constraint.ConstraintLayout>

```
画面遷移だけといった簡単な処理であれば、簡潔に記述が可能。

```kotlin:MainActivity.kt
package jp.co.casareal.sample.bmisample

import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v7.app.AlertDialog
import android.view.View
import jp.co.casareal.sample.bmisample.entity.PersonalData
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    ・・・省略・・・

    fun onClickBMIList(view: View) = startActivity(Intent(baseContext,BMIListActivity::class.java))

}

```


### 一覧画面
#### 一覧画面のレイアウトファイル

レイアウトとしては、ListViewを記述しているのみで、特殊な事はおこなっていない。

```xml:activity_bmilist.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="jp.co.casareal.sample.bmisample.BMIListActivity">

    <ListView
        android:id="@+id/list"
        android:layout_width="0dp"
        android:layout_height="wrap_content" />
</android.support.constraint.ConstraintLayout>
```

#### 一覧画面に表示するためのActivity

処理としては、データベースに登録したデータを、一覧画面に表示する処理を記述している。
処理の流れは以下の通り

1. データベースに接続するために、BMIDatabaseOpenHelperのオブジェクトを取得
2. データベースから登録されているデータを取得
3. カスタマイズしたアダプターのオブジェクトを生成
4. アダプターに対し、表示するデータを設定
5. listViewに対してカスタマイズしたAdapterを設定

```kotlin:BMIListActivity.kt
package jp.co.casareal.sample.bmisample

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import jp.co.casareal.sample.bmisample.adapter.BMIListAdapter
import jp.co.casareal.sample.bmisample.bo.ListDataParser
import jp.co.casareal.sample.bmisample.db.BMIDatabaseOpenHelper
import jp.co.casareal.sample.bmisample.entity.ListData
import kotlinx.android.synthetic.main.activity_bmilist.*
import org.jetbrains.anko.db.select

class BMIListActivity : AppCompatActivity() {


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

        val helper = BMIDatabaseOpenHelper.getInstance(this)
        val dataList =  helper.readableDatabase.select(BMIDatabaseOpenHelper.tableName).parseList<ListData>(ListDataParser())

        list.adapter = BMIListAdapter(baseContext, R.layout.row).apply {
            addAll(dataList)
        }
    }

}
```

「1. データベースに接続するために、BMIDatabaseOpenHelperのオブジェクトを取得」は下記の記述方法

```kotlin
val helper = BMIDatabaseOpenHelper.getInstance(this)
```

「2. データベースから登録されているデータを取得」は下記の記述方法となる。

```kotlin
val dataList =  helper.readableDatabase.select(BMIDatabaseOpenHelper.tableName).parseList<ListData>(ListDataParser())
```

下記で、readbleDatabaseオブジェクトを取得

```kotlin
helper.readableDatabase
```

下記で実際に実行するSQLを構築している。しかし、この時点では実行されない。引数にテーブル名を指定する必要がある。
この時点でのSQLは【select * from テーブル名】と同義。
メソッドチェインでWhere句なども追加も可能。

```kotlin
select(BMIDatabaseOpenHelper.tableName)
```

parseListメソッドを呼び出す事によって実行され、検索結果をパースする。ジェネリクスでパースした後の型を指定する。
引数にはパースの処理を記述したクラスのオブジェクトを指定する必要がある。
リストではなく、単一オブジェクトを返すメソッドも存在する。

```kotlin
parseList<ListData>(ListDataParser())
```

「3. カスタマイズしたアダプターのオブジェクトを生成
4. アダプターに対し、表示するデータを設定
5. listViewに対してカスタマイズしたAdapterを設定」に該当する処理は下記となる。

```kotlin
list.adapter = BMIListAdapter(baseContext, R.layout.row).apply {
            addAll(dataList)
}
```

アダプターの生成箇所は下記

```kotlin
BMIListAdapter(baseContext, R.layout.row)
```

アダプターに表示するデータの設定箇所は下記

```kotlin
.apply {
            addAll(dataList)
}
```

ListViewにアダプターを設定している箇所は下記

```kotlin
list.adapter = BMIListAdapter
```

# 2018/01/16追記
文字列の定義がないため、文字列定義のxmlを追記します。

```xml:strings.xml
<resources>
    <string name="app_name">BMISample</string>
    <string name="label_input_weight">体重（kg）：</string>
    <string name="label_input_height">身長（cm）：</string>
    <string name="label_button_calc">計算</string>
    <string name="label_result_bmi">あなたのBMI：</string>
    <string name="label_result_kind">あなたの体型：</string>
    <string name="result_kind_thin">痩せ型</string>
    <string name="result_kind_normal">標準</string>
    <string name="result_kind_fat">肥満</string>
    <string name="dialog_title_invalid_input">入力に誤りがあります。</string>
    <string name="btn_register_database">DB登録</string>
    <string name="btn_bmi_list">過去のBMI一覧表示</string>
    <string name="label_result_date">日付：</string>
</resources>
```

# まとめ
Kotlinで実装する事により、Javaで記述するより簡潔に記述出来る箇所が多いように感じる。

# 参考
https://github.com/Kotlin/anko/wiki/Anko-SQLite