KotlinでGsonを使う
Gsonを使う準備
Gsonは様々なメーカーから様々な機能を持ったGSonライブラリが提供されている。
便利なのを使うべきなのだろうけど最初から入っているGoogle社のGsonをここでは使用する。
※どうせカプセル化してしまえばあまり関係ないし
プロジェクトツリーのbuild.gradle(app)を開いて dependencies { の中に
implementation 'com.google.code.gson:gson:2.8.6'
を追加して更新するとGsonが使えるようになります。
値を管理するクラスを作る
Kotlinで値を管理するためのクラスを作ります。
open class gsonData(){
var dStr : String = "test"
var dInt : Int = 1234
var dDouble : Double = 5.6
}
作ったクラスは
val gsonData = gsonData()
このように宣言すれば使えます。
クラス内の変数にアクセスしてみましょう。
gsonData.dStr = "hello"
gsonData.dInt = 9876
gsonData.dDouble = 0.0
Gson形式で保存する
Gson形式の文字列に変換するには
val str = Gson().toJson(gsonData)
とします。
strの中身は
{"dDouble":123.4567,"dInt":987,"dStr":"testFile5"}
このようになっています。
Gson形式での保存では値だけではなく宣言した変数名も保存されていますので、クラス内の変数の順番や数が変わってもある程度互換性があるまま読み込めそうですね。
Gson形式の文字列をクラスに反映させる
strにGson形式のデータが入っていてそれを反映させる場合は
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を利用すると上手くいきます。
fun toGsonString() : String{
val str = Gson().toJson(this)
return str
}
しかしgsonData = Gson().fromJson(str, gsonData::class.java) as gsonDataというようにGson().fromJsonの返り値がデータクラスの返り値でした。ではそれをクラスのメソッドの中に入れると
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が必要です。では初期化のときの引数にしてしまえば良いと思い
open class gsonData(private val context: Context){
と定義して
val gsonData = gsonData(this)
と定義すれば良いような気がしますが今度は
fun toGsonString() : String{
val str = Gson().toJson(this)
return str
}
が実行時エラーになりますが原因はさっぱりわかりません。
悩み続けても仕方が無いので
1.データはデータだけのクラスに管理させる
2.データのファイル操作クラスにはContextを渡す
3.データを直接参照させたいのでファイル操作クラスはデータクラスを継承させる
このような設計思想とします。
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文字列からの反映に利用しています。
変数定義の他に代入命令の追加が必要になりますが妥協点とします。
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許容としています。
文字列定義は
<?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>
画面デザインは
<?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部分は
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に慣れてきたらもっと簡単に書けないか考えてみたいと思います。