10
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-09-25

検証環境

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

  • 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の一覧表示

完成イメージは以下の通り。
完成イメージ.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から取得した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行分に該当するデータクラス
一覧表示にも使用する

ListData
package jp.co.casareal.sample.bmisample.entity

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

データベース

BMIDatabaseOpenHelper

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

今回作成するデータベースは以下の通り。
er.png

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クラスと同様に以下の順で値を渡している。

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に対し、カラム名を指定してデータを取得する。

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を継承する
ジェネリクスでパース後のクラスの型を指定する

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行分のレイアウト
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メソッドの第2引数のnullチェックはエルビス演算子を使用することにより完結になっている。
また、表示するデータを取得したときのnullチェックもletを使用するこにより、より簡潔に記述できる。

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をデータベースに登録する処理を追加

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

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に接続して登録する処理を記述した。
※前回との差分のみ掲載。

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メソッドみたいにアクセスするには「クラス名.メソッド名」で呼び出す。

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())
))

入力画面

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

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

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>

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

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を記述しているのみで、特殊な事はおこなっていない。

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を設定
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のオブジェクトを取得」は下記の記述方法

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を追記します。

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で記述するより簡潔に記述出来る箇所が多いように感じる。

参考

10
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?