この記事は、Kotlin Advent Calendar 2016 21日目の記事です。
AndroidにはIcePickというライブラリがあり、これは @State
というアノテーションをプロパティに付加することでAndroidのActivityやFragmentのフィールドインスタンスを保存復元することができるライブラリです。
以下のように使います。
class ExampleActivity extends Activity {
@State String username; // This will be automatically saved and restored
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState);
}
@Override public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
}
しかし、このライブラリはKotlinで作られたActivity/Fragmentではそのままでは正しく動作しません。
実は保存したいフィールドに @JvmField
をつけることでIcePickでも使えるのですが、@JvmField
をつけるとsetter/getterではないそのままのプロパティを公開しないといけなくなる、という気持ち悪さがあります。
Pikkel
そこで、KotlinでもIcePickのように簡単にフィールドの変数を保存/破棄できるようにPikkelというライブラリを作りました。
インストール方法
JitPack経由でインストールします。
repositories {
maven { url "https://jitpack.io" }
}
dependencies {
compile 'com.github.yamamotoj:pikkel:0.3.3'
}
利用方法
利用方法は以下のコードの通りです。
class MainActivity : AppCompatActivity(), Pikkel by PikkelDelegate() { // Implement Pikkel interface with PikkelDelegate class delegation.
var data by state<String?>(null) // This will be automatically saved and restored
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
restoreInstanceState(savedInstanceState) // Saved states are restored here by Pikkel
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
saveInstanceState(outState) // Save states here by Pikkel
}
}
- まず、利用したいActivity/Fragmentで
Pikkel by PikkelDelegate()
とPikkelのインターフェイスとそのdelegateクラスを class delegationを用いて実装します。 - 次に保存したいプロパティを
by state(初期値)
のように定義します。 - あとは
saveInstanceState()
メソッドを使って保存、restoreInstanceState()
メソッドを使って復元です。
これだけで、IcePickと同じようにインスタンス変数の保存/復元をすることが出来ます。
しかもIcePickとちがって保存する変数を private
にすることも可能です。
実装をみてみる
Pikkelのコードは以下の通り。import部を除くと約50行ちょっとです。
interface Pikkel {
val pikkelBundle: Bundle
fun restoreInstanceState(savedInstanceState: Bundle?) {
savedInstanceState ?: return
pikkelBundle.putAll(savedInstanceState)
pikkelBundle.keySet().filter { !it.startsWith("pikkel:") }.forEach { pikkelBundle.remove(it) }
}
fun saveInstanceState(outState: Bundle?) {
outState ?: return
outState.putAll(pikkelBundle)
}
fun <T> state(initial: T): ReadWriteProperty<Pikkel, T> = State(initial)
private class State<T>(private val initial: T) : ReadWriteProperty<Pikkel, T> {
override fun getValue(thisRef: Pikkel, property: KProperty<*>): T {
val key = "pikkel:" + property.name
if (!thisRef.pikkelBundle.containsKey(key)) {
return initial
} else {
@Suppress("UNCHECKED_CAST")
return thisRef.pikkelBundle.get(key) as T
}
}
override fun setValue(thisRef: Pikkel, property: KProperty<*>, value: T) {
val key = "pikkel:" + property.name
when (value) {
is Bundle -> thisRef.pikkelBundle.putBundle(key, value)
is Int -> thisRef.pikkelBundle.putInt(key, value)
is Byte -> thisRef.pikkelBundle.putByte(key, value)
is ByteArray -> thisRef.pikkelBundle.putByteArray(key, value)
is Boolean -> thisRef.pikkelBundle.putBoolean(key, value)
is BooleanArray -> thisRef.pikkelBundle.putBooleanArray(key, value)
is Char -> thisRef.pikkelBundle.putChar(key, value)
is CharArray -> thisRef.pikkelBundle.putCharArray(key, value)
is Float -> thisRef.pikkelBundle.putFloat(key, value)
is FloatArray -> thisRef.pikkelBundle.putFloatArray(key, value)
is Parcelable -> thisRef.pikkelBundle.putParcelable(key, value)
is Short -> thisRef.pikkelBundle.putShort(key, value)
is ShortArray -> thisRef.pikkelBundle.putShortArray(key, value)
is String -> thisRef.pikkelBundle.putString(key, value)
is CharSequence -> thisRef.pikkelBundle.putCharSequence(key, value)
is Serializable -> thisRef.pikkelBundle.putSerializable(key, value)
null -> thisRef.pikkelBundle.putString(key, null)
else -> throw IllegalArgumentException()
}
}
}
}
class PikkelDelegate() : Pikkel {
override val pikkelBundle: Bundle = Bundle()
}
ポイント1: PikkelインターフェイスとPikkelDelegateクラス
Pikkel
はPikkelインターフェイスとそれを実装するPikkelDelegateクラスで構成されており、利用するActivity/Fragmentでは Pikkel by PikkelDelegate()
という形で利用することになります。これはKotlinの class delegation
という機能でこれにより利用する側では余計なクラスを継承することなく、Pikkelの実装を利用することが出来ます。また、使用するメソッドはすべてPikkelインターフェイスでのみ実装されているため、それ以外のクラスからは参照される心配はありません。
ポイント2: Delegated Property
保存するプロパティに指定する by state()
はKotlinの delegated property
という機能を使用しています。これは、変数の宣言時に ReadWriteProperty
インターフェイスを実装したクラスを返すことでそのプロパティに値が set/get
されたときの処理をカスタマイズすることができる、というものです。 Pikkelでは by state()
で指定されたプロパティが設定されるときにPikkelDelegateが持つ Bundle
に値をセットし、 saveInstanceState()
restoreInstanceState()
で保存/復元することでIcePickと同等の処理を実現しています。
制限事項
保存時に値の型を見て、Bundleに保存しているため、ジェネリクス型が含まれた変数の型を特定することができません。したがってBundleに存在している putSparseParcelableArray()
や putStringArrayList()
に対応する値を保存することが出来ません。
おわりに
このライブラリは弊社でリリースしているアプリEightでも使用していますので、ぜひ使用してみてください。
こんなに少ない行数で便利なライブラリが作れちゃうKotlinサイコーです!