KotlinでAndroidアプリを開発（BMI計算アプリ作成編）

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

* Java:8
* Android studio 2.3
* CompileSdkVersion:25
* MinSdkVersion:19
* TargetSdkVersion:25
* BuildToolsVersion:25.0.2

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

* 様々な文法を試す
* 簡単なAndroidアプリの開発方法
* Viewのプロパティへのアクセス
* 文字列→数値変換
* 画面遷移
* 画面遷移の際のパラメータ取得

# アプリの内容
身長（cm）と体重（kg）を入力して、ボタンを押すと画面遷移する。（以降、入力画面と表記する）
遷移先の画面では、BMIを計算し、普通、肥満、痩せ型を判定する。（以降、結果画面と表記する）

完成イメージは以下の通り。


# 実装内容

## パッケージ情報

| パッケージ名|格納ファイル|
|:-----------:|:----:|
|jp.co.casareal.sample.bmisample|Activityを格納|
|jp.co.casareal.sample.bmisample.entity|データクラスを格納|

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

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

### 入力画面
|ファイル名|概要|
|:-----------:|:-----------------------------:|
|jp.co.casareal.sample.bmisample.MainActivity|入力画面のActivityとボタンが押された時の処理|
|activity_main.xml|入力画面のレイアウトを定義したファイル|

### 結果画面
|ファイル名|概要|
|:-----------:|:-----------------------------:|
|jp.co.casareal.sample.bmisample.ResultActivity|結果画面のActivity|
|jp.co.casareal.sample.bmisample.entity.PersonalData|結果画面のレイアウトを定義したファイル|

## コードと説明
### 共通
#### データクラス
入力画面で入力された値を保持し、結果画面にデータを受け渡すためのデータクラス
格納されている身長と体重で計算したBMI保持するプロパティを保持する

```kotlin:PersonalData.kt
package jp.co.casareal.sample.bmisample.entity

import java.io.Serializable

data class PersonalData(var tall: Double, var weight: Double) : Serializable {
    val bmi: Double? by lazy {
        weight / Math.pow(tall / 100, 2.0)
    }
}
```
classキーワードの前にdataキーワードを記述することによって、データモデルとなる。
継承したり、抽象クラスにはできないがインターフェイスを実装することは許可されている。
今回はIntentに格納してデータを受け渡すために、対象クラスでSerializableを実装している。

クラス名の直後に丸括弧「()」で囲った箇所に変数を定義することでプロパティとなる。

lazyを使用することにより、初めてそのプロパティを参照したときに実行される処理を記述できる



#### 文字列定義ファイル
作成したアプリで使用する文字列を定義

```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>
</resources>
```

### 入力画面
#### 画面のレイアウトファイル
入力画面のレイアウトファイルは以下を作成した。

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


    <TextView
        android:id="@+id/weight_label"
        android:layout_width="145dp"
        android:layout_height="30dp"
        android:layout_marginLeft="28dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="44dp"
        android:text="@string/label_input_weight"
        android:textAlignment="textEnd"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/editTextWeight"
        android:layout_width="147dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="36dp"
        android:inputType="numberDecimal"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.461"
        app:layout_constraintStart_toEndOf="@+id/weight_label"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="145dp"
        android:layout_height="30dp"
        android:layout_marginBottom="8dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="8dp"
        android:text="@string/label_input_height"
        android:textAlignment="textEnd"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/weight_label"
        app:layout_constraintVertical_bias="0.087" />

    <EditText
        android:id="@+id/editTextTall"
        android:layout_width="147dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:ems="10"
        android:inputType="numberDecimal"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.461"
        app:layout_constraintStart_toEndOf="@+id/textView"
        app:layout_constraintTop_toBottomOf="@+id/editTextWeight"
        app:layout_constraintVertical_bias="0.046" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:onClick="onClickCalcButton"
        android:text="@string/label_button_calc"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.384"
        tools:text="@string/label_button_calc"
         />
</android.support.constraint.ConstraintLayout>
```
属性の名称と概要は以下の通り


|名称|説明|
|:-----|:---|
|layout_constraint***XXX***_to***XXX***Of|Viewの配置場所を相対的に設定可能。他のViewのリソースIDを指定することによって。配置場所を決定する<br>XXXには以下が記入可能<br>・Start<br>・End<br>・Right<br>・Left<br>・Top<br>・Bottom|
|layout_margin***XXX***|隣接するViewからのマージンを設定可能。<br>XXXには以下が記入可能<br>・Start<br>・End<br>・Right<br>・Left<br>・Top<br>・Bottom|
|layout_constraint***xxx***_bias|隣接するViewとのマージンを差し引いた余白をマージンに加える時の割合を指定可能。XXXには以下が記入可能<br>・Vertical<br>・Horizontal|
|onClick|対象のViewが押下されたときに呼び出されるメソッドの定義（文字列は任意だが、最終的にはメソッド名になるため、メソッド名の命名規約に基づいた名前にする必要がある）


#### 入力画面のActivity
このActivityでは、以下の処理を実施
1. 入力された身長と体重を取得
2. 文字列から数値に変換
3. 不正な値が入力された場合はダイアログを表示
4. 入力された値をデータクラスに格納
5. 結果画面に遷移

```kotlin:jp.co.casareal.sample.bmisample.MainActivity
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() {

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

    fun onClickCalcButton(view: View) {

        val tall = editTextTall.text.toString().toDoubleOrNull()
        val weight = editTextWeight.text.toString().toDoubleOrNull()

        if (tall == null || weight == null) {
            AlertDialog.Builder(this).setTitle(R.string.dialog_title_invalid_input).setPositiveButton(android.R.string.ok, null).show()
            return
        }

        val data = PersonalData(tall, weight)

        startActivity(Intent(this, ResultActivity::class.java).apply {
            putExtra("PersonalData", data)
        })

    }
}

```

このActivityで行っている処理の詳細は以下の通り

```kotlin
class MainActivity : AppCompatActivity()
```
:（コロン）でAppCompatActivityクラスを継承する

```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
}
```
MainActivityとレイアウトファイルの紐付け

```kotlin
 fun onClickCalcButton(view: View) {
　　・・・省略・・・
 }
```
レイアウトファイルのonClick属性に指定された名前で定義したメソッド。
この時のルールとして戻り値はなし、引数はView型の１つとなる。

```kotlin
   val tall = editTextTall.text.toString().toDoubleOrNull()
   val weight = editTextWeight.text.toString().toDoubleOrNull()

```
レイアウトファイルに配置したViewのid名で直接Viewにアクセスが可能。
処理としては、入力された値を取得している。その際、値は文字列で取得することになる。

toDoubleOrNullメソッドで文字列から数値（今回はDouble型）に変換している。
文字列から数値に変換できない場合、nullが返されるメソッド。

```kotlin
 if (tall == null || weight == null) {
  AlertDialog.Builder(this)
    .setTitle(R.string.dialog_title_invalid_input)
    .setPositiveButton(android.R.string.ok, null)
    .show()
  return
 }
```
数値変換に失敗した場合、tallとweight変数にはnullが格納されているので、
どちらかがnullの場合、ダイアログを表示し、このメソッドを終了させる。

```kotlin
val data = PersonalData(tall, weight)
```

PersonalData（データクラス）に数値変換した値を格納。

```kotlin
  startActivity(Intent(this, ResultActivity::class.java).apply {
            putExtra("PersonalData", data)
  })
```
startActivityメソッドの引数の中で、Intentを生成している。
Intentを生成し、画面遷移の準備を行う。
Intentのコンストラクタの第２引数に遷移先のActivityのクラス情報を渡す。
クラス情報を取得するには「クラス名::class.java」を記述する。
applyを使用することで、applyのブロックでは自身のオブジェクトの状態を変更できる。
今回は、putExtraメソッドでデータを格納している。
applyではそのオブジェクトそのものが戻り値となる。
pubExtraメソッドで遷移先の画面で使用する身長と体重が格納されているPersonalDataのオブジェクトを格納。
satartActivityメソッドで画面遷移を開始。


### 結果画面

### レイアウトファイル
結果画面のレイアウトファイルは以下を作成した。

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

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="32dp"
        android:layout_marginTop="44dp"
        android:text="@string/label_result_bmi"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/bmiTextView"
        android:layout_width="58dp"
        android:layout_height="0dp"
        android:layout_marginStart="32dp"
        android:layout_marginTop="44dp"
        android:text="TextView"
        app:layout_constraintStart_toEndOf="@+id/textView2"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="32dp"
        android:layout_marginTop="52dp"
        android:text="@string/label_result_kind"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView2" />

    <TextView
        android:id="@+id/kindTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="32dp"
        android:layout_marginTop="52dp"
        android:text="TextView"
        app:layout_constraintStart_toEndOf="@+id/textView4"
        app:layout_constraintTop_toBottomOf="@+id/bmiTextView" />
</android.support.constraint.ConstraintLayout>
```

#### 結果画面のActivity
このActivityでは、以下の処理を実施
1. Intentに格納されている身長と体重のデータクラスの取得
2. BMIを計算
3. BMIに対応した文字列を文字列リソースから取得
4. 文字列をTextViewに設定


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

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import jp.co.casareal.sample.bmisample.entity.PersonalData
import kotlinx.android.synthetic.main.activity_result.*

class ResultActivity : AppCompatActivity() {

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

        val data = intent.getSerializableExtra("PersonalData") as PersonalData

        data.bmi?.let {
            var message: String
            if (data.bmi!! < 18.5) {
                message = getString(R.string.result_kind_thin)
            } else if (data.bmi!! < 25.0) {
                message = getString(R.string.result_kind_normal)
            } else {
                message = getString(R.string.result_kind_fat)
            }
            bmiTextView.text = String.format("%.2f", data.bmi)
            kindTextView.text = message
        }
    }
}

```
このActivityで行っている処理の詳細は以下の通り

```kotlin
 val data = intent.getSerializableExtra("PersonalData") as PersonalData
```
PersonalDataという名前で格納した身長と体重のデータクラスを取得。
***as***キーワードで型を変換。
intentオブジェクトはKotlinの場合、getIntentメソッドを呼び出さなくても、
呼び出されてintentという変数に格納された状態になっている。

```kotlin
 data.bmi?.let {
 ・・・省略・・・
 }
```

bmiプロパティがnullでなければ処理を行う際の記述方法として
letキーワードを使用している。
data.bmi?.let{}と記述するこにより、bmiがnullでなければ中括弧内の処理を
行うという意味になる。

```kotlin
 var message: String
 if (data.bmi!! < 18.5) {
  message = getString(R.string.result_kind_thin)
 } else if (data.bmi!! < 25.0) {
  message = getString(R.string.result_kind_normal)
 } else {
  message = getString(R.string.result_kind_fat)
 }
```

getStringメソッドを呼び出し、文字列リソースから文字列を取得。
また、data.bmi!!はbmiプロパティがnullで無いことを明示的に示している。

```kotlin
 bmiTextView.text = String.format("%.2f", data.bmi!!)
 kindTextView.text = message
```

bmiの小数点第２位まで表示するため、フォーマットを行う。
TextViewのid属性に設定した「kindTextView」を使用して、表示する文字列を設定。

# 参考
Kotlin
https://kotlinlang.org/

Android Developers
https://developer.android.com/