Kotlin Advent Calendar 2015の11日目の記事です
昨日は red_fat_daruma さんによるJavaからの利用を視野に入れたKotlinコードで何をするべきかでした
久しぶりのブログ更新です。最近Androidをあまり触らずiOSに浮気をしています。
浮気をしてる最中なので正直何を書こうか悩みました。kotlinでアプリを作るとか。spring bootをkotlinで…とか色々考えましたが、どれもあまりkotlinらしい内容にできそうになかったので
なるべくkotlin特有のものを…と必死に考えた結果SharedPreferenceを使って拡張関数や拡張プロパティ、DelegateについてAndroidで実際に使えるであろう場面について書こうと思います。
拡張関数
一言で言うと、既存のクラスにメソッドを独自に拡張できます。
例えばMutableListクラスにswapメソッドを拡張して、引数で受け取ったindexの値を入れ替えるメソッドを作ります
1 2 3 4 5 |
fun <T> MutableList<T>.swap(index1: Int, index2: Int) { val tmp = this[index1] // 'this' corresponds to the list this[index1] = this[index2] this[index2] = tmp } |
1 2 3 |
val l = arrayListOf(1,2,3) //(1,2,3) //拡張関数呼び出す l.swap(0,2) //(3,2,1)に入れ替わる |
これだけでもちょっと便利だなぁという感じがしますが
もう少し実用的なやつで、AndroidのSharedPreferenceを拡張してみます。
多くの開発者がやるであろう、ObjectをJSON文字列にしてSharedPreferenceに保存するアレを拡張関数で定義してみます。
JSONのparserにはGSONを使っています。
1 2 3 4 5 6 7 8 9 |
//何かしらのObjectをJSON文字列に変換して保存するSharedPreferencesの拡張関数 public fun SharedPreferences.applyToJson(key: String, value: Any) { edit().putString(key, Gson().toJson(value)).apply() } //JSON文字列からObject復元するSharedPreferencesの拡張関数 public fun <T : Any> SharedPreferences.getFromJson(key: String,clazz:Class<T>):T { return Gson().fromJson(getString(key,""),clazz) } |
この拡張関数を実際に使ってみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class MainActivity : ActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val sharedPreference = getSharedPreferences("myPref",MODE_PRIVATE) //Userオブジェクトを拡張関数を使って保存 val toJsonUser = User("name",28) sharedPreference.applyToJson("user",toJsonUser) //UserオブジェクトをJSON文字列から復元 val fromJsonUser = sharedPreference.getFromJson("user",User::class.java) } } |
すごく使い勝手が良いんじゃないでしょうか?
これと同じことをJavaでやろうとすると、色々なところにGsonのシリアライズとデシリアライズのコードが出てきてしまったり、SharedPreferenceを継承したクラスを新たに作ったり、
はたまたSharedPreferenceのサービスクラスのようなものを実装しないといけません。
そして多くの場合ContextやActivityを引数として要求されて、嫌な気分になるのではないでしょうか?
拡張プロパティ
kotlinは関数だけでなくプロパティも拡張できます[公式より]
1 2 |
val <T> List<T>.lastIndex: Int get() = size - 1 |
kotlinは標準でListにlastIndexというプロパティを拡張しています。
1 2 3 |
val list:List<Int> = arrayListOf(1,2,3,4) print(list.lastIndex) // 3 print(list.get(list.lastIndex)) // 4 |
Listの終端を参照するのに毎回(size-1)をするより明示的でいいですね
これを参考にActivityにSharedPreferenceを返すプロパティを拡張してみます
返り値(Activity.(String) -> SharedPreferences)がちょっと複雑な形していますが、これはStringを引数として受け取ってSharedPreferencesを返す関数リテラルが返り値となっています。
1 2 |
public val Activity.prefInitializer: Activity.(String) -> SharedPreferences get() = { getSharedPreferences(it, Context.MODE_PRIVATE) } |
実際に使うと
1 2 3 4 5 6 7 8 9 |
class MainActivity : ActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val pref = prefInitializer("pref")//拡張プロパティを参照してる pref.applyToJson("user",User("name",28))//問題なく実行される } } |
とりあえずSharedPreference返すプロパティを作りました。getSharedPreferenceを呼ばなくもSharedPreferenceを取得できるようになりました。
ただこれに関しはもう普通にgetSharedPreferenceを呼ぶでいいと思います…
Delegate
続いてDelegateです。こちらに関してはアドベントカレンダーの二日目に投稿されていた記事が参考になりますみんな大好きKotlinのDelegationについて #ktac2015
kotlinでAndroidをやるからには固く実装するために、変数は可能な限りvalにしたいものです。
しかしなかなかAndroidはそれを許してくれません。
1 2 3 4 5 6 7 8 9 10 11 |
class HogeActivity : ActionBarActivity() { //このSharedPreferenceをvalにしたいし、そもそもOptionalにしたくない… var sharedPreference:SharedPreferences? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) sharedPreference = getSharedPreferences("pref",MODE_PRIVATE) } } |
そういうときに役に立つのがDelegateによる遅延初期化の機能です。
例えば標準で用意されているDelegates.NotNull()
これは以下のように実装されています
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public object Delegates { public fun notNull<T : Any>(): ReadWriteProperty<Any?, T> = NotNullVar() private class NotNullVar<T : Any>() : ReadWriteProperty<Any?, T> { private var value: T? = null public override fun getValue(thisRef: Any?, property: KProperty<*>): T { return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.") } public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { this.value = value } } } |
みての通りgetvalueが呼ばれたタイミング(プロパティにアクセスしたタイミング)でvalueがnullの場合はExceptionを投げるという実装です。
これを参考に自分でもひとつDelegateを作ってみます。
ジェネリクスを<T : Any>から<T : Activity>に変えて
getValueのところをExceptionを投げるというものからSharedPreferenceのインスタンスを作成するようにカスタマイズすればできそうです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//Activityを継承したクラスの拡張関数 public fun <T : Activity> bindSharedPreference(name:String): ReadOnlyProperty<T, SharedPreferences> = SharedDelegate(name) //自作したDelegationクラス //getValueしたときにNULLだったらSharedPreferenceを作ります private class SharedDelegate<T : Activity>(private val name: String) : ReadOnlyProperty<T, SharedPreferences> { private var value: SharedPreferences? = null override fun getValue(thisRef: T, property: KProperty<*>): SharedPreferences { if (value == null) { value = thisRef.getSharedPreferences(name, Context.MODE_PRIVATE); } return value as SharedPreferences } } |
この自作したDelegateを使ってみると
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class MainActivity : ActionBarActivity() { //valにできた val sharedPreference: SharedPreferences by bindSharedPreference("myPref") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //nullじゃない! sharedPreference.edit().putString("hoge","fuga") //再代入できない sharedPreference = getSharedPreferences("pref", Context.MODE_PRIVATE);//NG } } |
固い!
しかし残念ながらこれだけではActivityではSharedPreferenceをbindできますが
Fragment上ではまだできません。なのでFragmentでも使えるように上記の内容を踏まえてごりごりに拡張していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
import android.app.Activity import android.app.Fragment import android.content.Context import android.content.SharedPreferences import com.google.gson.Gson import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty //supportV4のFragmentはSupportFragmentリネイムしてます import android.support.v4.app.Fragment as SupportFragment //GSONを使ったSharedPreferencesの拡張関数 public fun SharedPreferences.applyToJson(key: String, value: Any) { edit().putString(key, Gson().toJson(value)).apply() } public fun <T : Any> SharedPreferences.getFromJson(key: String, clazz: Class<T>): T { return Gson().fromJson(getString(key, ""), clazz) } //ActivityやらFragmentの拡張関数 public fun Activity.bindSharedPreference(name: String) : ReadOnlyProperty<Activity, SharedPreferences> = required(name, prefInitializer) public fun Fragment.bindSharedPreference(name: String) : ReadOnlyProperty<Fragment, SharedPreferences> = required(name, prefInitializer) public fun SupportFragment.bindSharedPreference(name:String) : ReadOnlyProperty<SupportFragment, SharedPreferences> = required(name,prefInitializer) //SharedPreferencesを生成する拡張プロパティ //Stringを受け取ってSharedPreferencesを返す関数リテラルが帰る private val Activity.prefInitializer: Activity.(String) -> SharedPreferences get() = { getSharedPreferences(it, Context.MODE_PRIVATE) } private val Fragment.prefInitializer: Fragment.(String) -> SharedPreferences get() = { activity.getSharedPreferences(it, Context.MODE_PRIVATE) } private val SupportFragment.prefInitializer: SupportFragment.(String) -> SharedPreferences get() = { activity.getSharedPreferences(it, Context.MODE_PRIVATE) } //nameにSharedPreferencesのファイル名、initializerにStringを受け取ってSharedPreferencesを返す関数リテラル。結果SharedDelegateが帰る private fun <T> required(name: String, initializer: T.(String) -> SharedPreferences) = SharedDelegate (name,initializer) //自作のDelegateクラス。valueがnullの場合、コンストラクターで受け取った //ファイル名とprefInitializerでSharedPreferencesを生成します private class SharedDelegate<T>(private val name: String, val initializer: T.(String) -> SharedPreferences) : ReadOnlyProperty<T, SharedPreferences> { private var value: SharedPreferences? = null override fun getValue(thisRef: T, property: KProperty<*>): SharedPreferences { if (value == null) { value = thisRef.initializer(name) } return value as SharedPreferences } } |
頭がパニックになりそうですが、このファイルを用意するだけで
SharedPreferenceのJSONの拡張関数を利用することでき
Fragment,Activityで、Delegateでバインドすることができるようになります。
1 |
val pref:SharedPreferences by bindSharedPreference("hoge") //ActivityでもFragmentでも使える |
ここまで読んでいただけるなんとなく気づくであろうと思われすがこれらは基本
kotterknifeの実装を真似ています。
Androidでkotlinを使うとお世話になる方も多いのではないでしょうか?
1 |
val button:Button by bindView(R.id.button) //これ |
kotterknifeも中身はButterKnife.ktというファイルにActivityやFragmentの拡張関数や拡張プロパティ、Delegateクラスを用意しているだけです。
ただ用意しているだけとは言いましたが、kotlin初心者では中々解読できない代物でした…
なのでSharedPreferenceを使って真似て実装してみたのが今回のブログになります。
最後に
iOSに浮気していますが、kotlinをある程度勉強しているおかげでswiftがすごく読みやすいです。(たまにvar,val,letのパニック障害を起こしますが)
XCodeのサポートも多少の不満はあるものの、ほぼ問題ないレベルで補完が効くので開発しててストレスはあまり感じません。
AndroidとiOSの開発で言語の壁が大きかったのですが、swift,kotlinのどちらかを書ければ両方対応できる
そんな日も近いのではないでしょうか?
まだ趣味でしかkotlin書いてないので機会があれば次のプロダクトはkotlinでやりたいなぁ|ω・`)チラ
iOSに浮気を始めたAndroidエンジニア? Androidはほとんど書いてない…