AndroidStudioのKotlinでGsonを使う

#KotlinでGsonを使う
##Gsonを使う準備
Gsonは様々なメーカーから様々な機能を持ったGSonライブラリが提供されている。
便利なのを使うべきなのだろうけど最初から入っているGoogle社のGsonをここでは使用する。
※どうせカプセル化してしまえばあまり関係ないし

プロジェクトツリーのbuild.gradle(app)を開いて dependencies { の中に

```build.gradle(app)
    implementation 'com.google.code.gson:gson:2.8.6'
```

を追加して更新するとGsonが使えるようになります。

##値を管理するクラスを作る
Kotlinで値を管理するためのクラスを作ります。

```sample.kt
open class gsonData(){
    var dStr : String = "test"
    var dInt : Int = 1234
    var dDouble : Double = 5.6
}
```

作ったクラスは

```MainActivity.kt
val gsonData = gsonData()
```

このように宣言すれば使えます。
クラス内の変数にアクセスしてみましょう。

```MainActivity.kt
    gsonData.dStr = "hello"
    gsonData.dInt = 9876
    gsonData.dDouble = 0.0
```

##Gson形式で保存する

Gson形式の文字列に変換するには

```MainActivity.kt
    val str = Gson().toJson(gsonData)
```

とします。
strの中身は

```
{"dDouble":123.4567,"dInt":987,"dStr":"testFile5"}
```

このようになっています。
Gson形式での保存では値だけではなく宣言した変数名も保存されていますので、クラス内の変数の順番や数が変わってもある程度互換性があるまま読み込めそうですね。


##Gson形式の文字列をクラスに反映させる
strにGson形式のデータが入っていてそれを反映させる場合は

```MainActivity.kt
  gsonData = Gson().fromJson<gsonData>(str, gsonData::class.java) as gsonData
```

このようにします。

##Gson形式で保存／読込するクラスのカプセル化
クラスに必要な変数を定義しておいて、それが簡単に保存や復元ができると便利ですね。
というわけでこのクラスに

|メソッド名|説明|
|:----:|:---:|
|fun toGsonString() : String|Gson形式の文字列を返します|
|fun fromData(str : String)|Gson形式の文字を渡して値を復元します|
|fun fileSave(filename : String)|指定したファイル名でGson形式のファイルを作成します|
|fun fileLoad(filename : String)|指定したファイル名のGsonファイルを読み込み復元します|

4つのクラスを実装します。
たったこれだけのことなのですが**Android環境**と**Kotlin**に不慣れなため悪戦苦闘します。もっと良い方法があればコメント願います。

##問題点その1　自分自身にGson値を渡せない

Gson文字列として返すため thisを利用すると上手くいきます。

```sample.kt
    fun toGsonString() : String{
        val str = Gson().toJson(this)
        return  str
    }
```

しかし**gsonData = Gson().fromJson<gsonData>(str, gsonData::class.java) as gsonData**というように**Gson().fromJson**の返り値がデータクラスの返り値でした。ではそれをクラスのメソッドの中に入れると

```sample.kt
open class gsonData(){
    var dStr : String = "test"
    var dInt : Int = 1234
    var dDouble : Double = 5.6

    fun fromData(str : String){
        this =  Gson().fromJson<gsonData>(str, gsonData::class.java) as gsonData
    }
}
```

ダメですね、thisは読み込み専用クラスなのでしょう

##問題点その2　ファイルの読み書きにcontext: Contextが必要
ファイルの読み書きにはcontext: Contextが必要です。では初期化のときの引数にしてしまえば良いと思い

```sample.kt
open class gsonData(private val context: Context){
```

と定義して

```MainActivity.kt
val gsonData = gsonData(this)
```

と定義すれば良いような気がしますが今度は

```sample.kt
    fun toGsonString() : String{
        val str = Gson().toJson(this)
        return  str
    }
```

が実行時エラーになりますが原因はさっぱりわかりません。
悩み続けても仕方が無いので
1.データはデータだけのクラスに管理させる
2.データのファイル操作クラスにはContextを渡す
3.データを直接参照させたいのでファイル操作クラスはデータクラスを継承させる
このような設計思想とします。

```gsonData.kt
open class gsonData(){
    var dStr : String = ""
    var dInt : Int? = 0
    var dDouble : Double? = 0.0

    fun assign(a : gsonData){
        dStr = a.dStr
        dInt = a.dInt
        dDouble = a.dDouble
    }
    // クラスのデータを Gson形式の文字列に変換
    fun toGsonString() : String{
        val d = gsonData()
        d.assign(this)
        val str = Gson().toJson(d)
        return  str
    }
    // Gson形式の文字列をクラスのデータに反映
    fun fromData(str : String){
        val data =  Gson().fromJson<gsonData>(str, gsonData::class.java) as gsonData
        assign(data)
    }
}
```

クラスの変数定義の他に**assign**メソッドを作りました。このメソッドを通じて安全に代入が出来るようにしています。
Gson文字列の作成やGson文字列からの反映に利用しています。
変数定義の他に代入命令の追加が必要になりますが妥協点とします。

```gsonFile.kt
class gsonFile : gsonData{
    var context : Context? = null
    constructor (context2: Context) :super() {
        this.context = context2
    }
    // 指定したファイル名で保存
    fun fileSave(filename : String) {
        val str = super.toGsonString()
        File(context?.filesDir, filename).writer().use {
            it.write(str)
       }
    }
    // 指定したファイル名から読み込む
    fun fileLoad(filename : String){
        val readFile = File(context?.filesDir, filename)

        if(readFile.exists()){
            val str = readFile.bufferedReader().use(BufferedReader::readText)
            fromData(str)
        }
    }

}
```

上位クラスの**gsonData**にはContextが不要で、それを継承したクラス**gsonFile**には必要とする定義を作ると何故か複雑な事になりました。初期化時代入も出来なかったのでnull許容としています。

文字列定義は

```strings.xml
<?xml version="1.0" encoding="utf-8" ?>
<resources>
    <string name="app_name">GSonSample</string>
    <string name="btSave">保存</string>
    <string name="btLoad">読込</string>
    <string name="tvStr">String型</string>
    <string name="tvInt">Int型</string>
    <string name="tvDouble">Double型</string>
</resources>
```

画面デザインは

```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".MainActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/clButton"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:orientation="horizontal"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <Button
            android:id="@+id/btSave"
            style="@style/Widget.AppCompat.Button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onButtonClickSave"
            android:text="@string/btSave"
            app:layout_constraintStart_toStartOf="parent" />

        <Button
            android:id="@+id/btLoad"
            style="@style/Widget.AppCompat.Button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="onButtonClickLoad"
            android:text="@string/btLoad"
            app:layout_constraintStart_toEndOf="@+id/btSave" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/clEdit"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/clButton">

        <EditText
            android:id="@+id/etStr"
            android:layout_width="202dp"
            android:layout_height="46dp"
            android:ems="10"
            android:inputType="text"
            android:text="test"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.564"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <EditText
            android:id="@+id/etInt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ems="10"
            android:inputType="number"
            android:text="12"
            app:layout_constraintStart_toStartOf="@+id/etStr"
            app:layout_constraintTop_toBottomOf="@+id/etStr" />

        <TextView
            android:id="@+id/tvDouble"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/tvDouble"
            app:layout_constraintBaseline_toBaselineOf="@+id/etDouble"
            app:layout_constraintEnd_toStartOf="@+id/etDouble" />

        <TextView
            android:id="@+id/tvStr"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/tvStr"
            app:layout_constraintBaseline_toBaselineOf="@+id/etStr"
            app:layout_constraintEnd_toStartOf="@+id/etStr" />

        <TextView
            android:id="@+id/tvInt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/tvInt"
            app:layout_constraintBaseline_toBaselineOf="@+id/etInt"
            app:layout_constraintEnd_toStartOf="@+id/etInt" />

        <EditText
            android:id="@+id/etDouble"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ems="10"
            android:inputType="numberDecimal"
            android:text="34.0"
            app:layout_constraintStart_toStartOf="@+id/etInt"
            app:layout_constraintTop_toBottomOf="@+id/etInt" />

    </androidx.constraintlayout.widget.ConstraintLayout>

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

MainActivity部分は

```MainActivity.kt
class MainActivity : AppCompatActivity() {

    val gsonData = gsonDataFile(this)
    val filename : String = "test.txt"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
    // 編集中の内容をクラスに反映
    fun activityToData(){
        val etString = findViewById<EditText>(R.id.etStr)
        val etInt = findViewById<EditText>(R.id.etInt)
        val etDouble = findViewById<EditText>(R.id.etDouble)
        gsonData.dStr = etString.text.toString()
        gsonData.dInt = etInt.text.toString().toIntOrNull()
        if (gsonData.dInt == null){gsonData.dInt = 0}
        gsonData.dDouble = etDouble.text.toString().toDoubleOrNull()
        if (gsonData.dDouble == null){gsonData.dDouble = 0.0}
    }
    // クラスの内容を編集画面へ反映
    fun dataToActivity(){
        val etString = findViewById<EditText>(R.id.etStr)
        val etInt = findViewById<EditText>(R.id.etInt)
        val etDouble = findViewById<EditText>(R.id.etDouble)
        etString.setText(gsonData.dStr.toString())
        etInt.setText(gsonData.dInt.toString())
        etDouble.setText(gsonData.dDouble.toString())
    }
    // 保存ボタンクリック
    fun onButtonClickSave(view : View){
        activityToData()
        gsonData.fileSave(filename)
    }
    // 読込ボタンクリック
    fun onButtonClickLoad(view : View){
        gsonData.fileLoad(filename)
        dataToActivity()
    }
}
```

このようになっています。

##最後に
データが入っているクラスをGson文字列に変換したり復元したりすることは保存以外にも色々利用価値があります。でも肝心のクラスをカプセル化しようとすると複雑になってしまいました。

AndroidやKotlinに慣れてきたらもっと簡単に書けないか考えてみたいと思います。