検証環境
この記事の内容は、以下の環境で検証しました。
- 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
アプリの内容
下記の記事で作成したアプリに改良を加える
今回は以下の機能を追加
- 計算したBMIとその時の年月日時をDBに登録
- 登録したBMIの一覧表示
実装内容
パッケージ情報
パッケージ名 | 格納ファイル |
---|---|
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から取得した1行分を解析するクラス |
アダプター
ファイル名 | 概要 |
---|---|
jp.co.casareal.sample.bmisample.adapter.BMIListAdapter | ListViewのカスタムアダプター |
入力画面
ファイル名 | 概要 |
---|---|
jp.co.casareal.sample.bmisample.MainActivity | 入力画面のActivity。 ボタンが押された時の処理 登録されたBMIの一覧表示画面に遷移するボタンの処理 |
layout/activity_main.xml | 入力画面のレイアウトを定義したファイル |
結果画面
ファイル名 | 概要 |
---|---|
jp.co.casareal.sample.bmisample.ResultActivity | 結果画面のActivity 計算したBMIを登録するボタンの処理 |
layout/activity_result.xml | 結果画面のレイアウトを定義したファイル |
一覧画面
ファイル名 | 概要 |
---|---|
jp.co.casareal.sample.bmisample.BMIListActivity | 一覧画面のActivity DBに登録されているデータを表示 |
layout/activity_bmilist.xml | 一覧画面のレイアウトを定義したファイル |
layout/row.xml | ListViewに表示する一行分のレイアウトファイル |
コードと説明
前回の記事で説明した部分は省略する。
(前回の記事:http://qiita.com/naoi/items/5cdaa72e4d8903b6a36e)
差分のみを説明する。
gradle
今回はAnkoを利用するのでモジュールのgradleに以下を追記
AnkoはJetBrains公式のライブラリ
様々な便利な機能が入っている
今回はSQLiteに関する機能を使用する
dependencies {
・・・省略・・・
compile 'org.jetbrains.anko:anko:0.10.1'
compile "org.jetbrains.anko:anko-sqlite:0.10.1"
}
共通
データクラス
データベースの1行分に該当するデータクラス
一覧表示にも使用する
package jp.co.casareal.sample.bmisample.entity
data class ListData(var bmi:String,var date:String)
データベース
BMIDatabaseOpenHelper
データベース・アクセスに必要となるクラス
処理としては、自身のオブジェクトをSingletonとしている。
また、テーブルの作成をしている。
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クラスと同様に以下の順で値を渡している。
class BMIDatabaseOpenHelper (context:Context):ManagedSQLiteOpenHelper(context,"bmidb.db",null,1)
順番 | 説明 |
---|---|
第1引数:Context | 言わずと知れたContextクラス |
第2引数:String | データベースファイル名 |
第3引数:CursorFactory | カスタマイズしたCursorを使用したい時に使用する。今回はnull |
第4引数:Int | データベースファイルのバージョン |
このクラスのオブジェクトをキャッシュするためにCompanion Objectを使用している
Javaで言うところのstaticメソッドやstatic変数に該当する
tableNameという変数名にテーブル名を定義し、static変数のように扱えるようにした。
instanceという変数名にこのクラスのオブジェクトを格納させ、Singletonの作りにしている。
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の場合はオブジェクトを生成して返している。
return instance ?: BMIDatabaseOpenHelper(context.applicationContext)!!
onCreateメソッドではテーブルの作成を行っている。
ankoではテーブル作成のメソッドを用意しており。より簡単にテーブルが作成出来るようになっている。
override fun onCreate(db: SQLiteDatabase?) {
db?.run { createTable(tableName,ifNotExists = true,
columns = *arrayOf( "date" to TEXT, "bmi" to TEXT))
}
}
テーブル作成では、SQLiteDatabaseのオブジェクトを使用する。その時にrun拡張関数を使用している。
nullでなければ、SQLiteDatabaseクラスのcreateTableメソッドで、テーブルを作成するようにしている。
db?.run {
}
テーブル作成ではcreateTableメソッドを使用している。
カラム名と型を指定している箇所で使用しているarrayOfにアスタリスクを指定している。これはvarargで値を渡す時に事前に配列を作成したばあいは、その変数やarrayOfにアスタリスクが必要となる。
createTable(tableName,ifNotExists = true,
columns = *arrayOf( "date" to TEXT, "bmi" to TEXT))
引数と意味は以下の通り。
順番 | 説明 |
---|---|
第1引数:String | テーブル名 |
第2引数:Boolean | ifNotExistsの有効無効をbooleanで指定する |
第3引数:vararg columns: Pair) | カラム名と型を指定する。 型名は |
今回は使用しないため、onUpgradeメソッドはから実装とした。
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
}
ListDataParser
データベースで検索した結果の1行分をデータクラスに格納するためのクラス。
使用場面は後述とするが、引数で受け取ったColumnsに対し、カラム名を指定してデータを取得する。
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を継承する
ジェネリクスでパース後のクラスの型を指定する
class ListDataParser : MapRowParser<ListData>
引数のcolumnsには検索結果が格納されている。
map型であるため、カラム名を指定して取得する。
また、Anyとしてとれるため、データクラスのプロパティに格納するときは型変換が必要となる。
override fun parseRow(columns: Map<String, Any?>): ListData {
return ListData(columns["bmi"] as String, columns["date"] as String)
}
アダプター
ListViewで表示する1行分のレイアウト
<?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メソッドの第2引数のnullチェックはエルビス演算子を使用することにより完結になっている。
また、表示するデータを取得したときのnullチェックもletを使用するこにより、より簡潔に記述できる。
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 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に接続して登録する処理を記述した。
※前回との差分のみ掲載。
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メソッドみたいにアクセスするには「クラス名.メソッド名」で呼び出す。
var helper = BMIDatabaseOpenHelper.getInstance(this);
データベースにデータを登録するときBMIDatabaseOpenHelperクラスのオブジェクトを使用する。データベースに接続した場合、closeメソッドを呼び出す事が必要となる。
ここで有用なのがuseとなる。
useを使用することにより、Closeable インタフェースを実装したクラスのcloseメソッドを処理終了後に、closeを半強制的にしてくれる。そのため、ここではuseを使用している。
helper.use {
・・・省略・・・
}
insertメソッドを使用することでデータベースに登録ができる。
また、登録するデータはto関数でカラム名と登録するデータとしておくことが可能。to関数を使用するとtoでつなげた2つの値で、Pairを生成してくれる。
引数は2つで第1引数にはテーブル名をしていする。第2引数に登録するデータを指定する。
insert(BMIDatabaseOpenHelper.tableName,*arrayOf(
"bmi" to bmiTextView.text,"date" to format.format(System.currentTimeMillis())
))
入力画面
一覧画面に遷移する処理の追加
一覧画面に遷移するボタンと処理を追加
※差分のみを記載
<?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>
画面遷移だけといった簡単な処理であれば、簡潔に記述が可能。
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 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
処理としては、データベースに登録したデータを、一覧画面に表示する処理を記述している。
処理の流れは以下の通り
- データベースに接続するために、BMIDatabaseOpenHelperのオブジェクトを取得
- データベースから登録されているデータを取得
- カスタマイズしたアダプターのオブジェクトを生成
- アダプターに対し、表示するデータを設定
- listViewに対してカスタマイズしたAdapterを設定
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のオブジェクトを取得」は下記の記述方法
val helper = BMIDatabaseOpenHelper.getInstance(this)
「2. データベースから登録されているデータを取得」は下記の記述方法となる。
val dataList = helper.readableDatabase.select(BMIDatabaseOpenHelper.tableName).parseList<ListData>(ListDataParser())
下記で、readbleDatabaseオブジェクトを取得
helper.readableDatabase
下記で実際に実行するSQLを構築している。しかし、この時点では実行されない。引数にテーブル名を指定する必要がある。
この時点でのSQLは【select * from テーブル名】と同義。
メソッドチェインでWhere句なども追加も可能。
select(BMIDatabaseOpenHelper.tableName)
parseListメソッドを呼び出す事によって実行され、検索結果をパースする。ジェネリクスでパースした後の型を指定する。
引数にはパースの処理を記述したクラスのオブジェクトを指定する必要がある。
リストではなく、単一オブジェクトを返すメソッドも存在する。
parseList<ListData>(ListDataParser())
「3. カスタマイズしたアダプターのオブジェクトを生成
4. アダプターに対し、表示するデータを設定
5. listViewに対してカスタマイズしたAdapterを設定」に該当する処理は下記となる。
list.adapter = BMIListAdapter(baseContext, R.layout.row).apply {
addAll(dataList)
}
アダプターの生成箇所は下記
BMIListAdapter(baseContext, R.layout.row)
アダプターに表示するデータの設定箇所は下記
.apply {
addAll(dataList)
}
ListViewにアダプターを設定している箇所は下記
list.adapter = BMIListAdapter
2018/01/16追記
文字列の定義がないため、文字列定義の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で記述するより簡潔に記述出来る箇所が多いように感じる。
参考