KotlinでもIcePickが使いたい!

  • 19
    いいね
  • 0
    コメント

この記事は、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サイコーです!

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