KotlinでもIcePickが使いたい！

この記事は、[Kotlin Advent Calendar 2016](http://qiita.com/advent-calendar/2016/kotlin) 21日目の記事です。

AndroidにはIcePickというライブラリがあり、これは `@State` というアノテーションをプロパティに付加することでAndroidのActivityやFragmentのフィールドインスタンスを保存復元することができるライブラリです。
以下のように使います。

```java
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](https://github.com/yamamotoj/Pikkel)というライブラリを作りました。

## インストール方法

JitPack経由でインストールします。

```gradle
repositories {
  maven { url "https://jitpack.io" }
}
```

```gradle
dependencies {
  compile 'com.github.yamamotoj:pikkel:0.3.3'
}
```

## 利用方法

利用方法は以下のコードの通りです。

```kotlin
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行ちょっとです。

```kotlin
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](https://8card.net/)でも使用していますので、ぜひ使用してみてください。
こんなに少ない行数で便利なライブラリが作れちゃうKotlinサイコーです！

Pikkel:
https://github.com/yamamotoj/Pikkel