Parcelize拡張概要
- Androidアプリ開発者にとってメンドウな
Parcelable
の定形実装を自動化できる - Annotationをつけるだけで、フィールドを全部Parcelableに突っ込んでくkれる
- 突っ込めないオブジェクトは警告が出る
- Annotationで除外を指定できる
Example
// これだけでParcelableを実装できる // BundleにもIntentにも突っ込める @Parcelize class ExampleUser( val name: String ) : Parcelable
何が問題か?
- 生成されたオブジェクトをアプリ間で受け渡す際に、シリアライズ・デシリアライズの問題が発生する恐れがあるのでは?と考えた
- フィールドが増減した時、フィールドの記述順が変更になった時、コイツは死ぬのでは?
- 例えば、自社のアプリ間で通信したい場合とか。
問題のある例
// アプリAではこのバージョンのクラスを使用している // ライブラリVersion 1.0 @Parcelize class ExampleUser( val name: String ) : Parcelable
// アプリBでは、ちょっと機能が増えた // ライブラリVersion 1.1 @Parcelize class ExampleUser( val name: String, val age: Int // 年齢フィールドを増やしたぞ ) : Parcelable
- この状態で、アプリAからアプリBにデータを渡すとどうなる?
- 多分死ぬ
現状を確認する
生成されるClassをjadる
- こんなCreatorが自動生成される
in.readString()
している
public final class ExampleUser implements Parcelable { public static class Creator implements android.os.Parcelable.Creator { public final Object[] newArray(int size) { return new ExampleUser[size]; } public final Object createFromParcel(Parcel in) { Intrinsics.checkParameterIsNotNull(in, "in"); return new ExampleUser(in.readString()); } public Creator() { } } }
フィールドを増やすとどうなるか?
- 案の定、順番通りにreadする
- このタイミングでデータ長が足りずに壊れる
public final Object createFromParcel(Parcel in) { Intrinsics.checkParameterIsNotNull(in, "in"); return new ExampleUser(in.readString(), in.readInt()); }
順番が変わるとどうなる?
- 当然、順番が変わる
- Classは死ぬ
public final Object createFromParcel(Parcel in) { Intrinsics.checkParameterIsNotNull(in, "in"); return new ExampleUser(in.readInt(), in.readString()); }
どうするか?
- 外見仕様を変えずに、互換性を何とかする
- Parcelable互換で、高機能な
Bundle
とカスタムプロパティでなんとかする bundle.get
をすることで、古いデータ仕様の場合はdefaultを詰め込むことができる
@Parcelize class ExampleUser internal constructor( private val bundle: Bundle ) : Parcelable { constructor(name: String, age: Int) : this( bundleOf( Pair("name", name), Pair("age", age) ) ) val age: Int get() = bundle.getInt("age", -1) val name: String get() = bundle.getString("name")!! }
// jadるとこんな感じになる public final int getAge() { return bundle.getInt("age", -1); } public final String getName() { String s = bundle.getString("name"); s; if(s == null) Intrinsics.throwNpe(); return; } public ExampleUser(Bundle bundle) { Intrinsics.checkParameterIsNotNull(bundle, "bundle"); super(); this.bundle = bundle; } public ExampleUser(String name, int age) { Intrinsics.checkParameterIsNotNull(name, "name"); this(BundleKt.bundleOf(new Pair[] { new Pair("name", name), new Pair("age", Integer.valueOf(age)) })); }
Bundle in Bundleすると死ぬ
単純に実装すると、 Bundle.putParcelable()
で突っ込んだBundleが死ぬ。
復元時に ClassLoader
が読み込まれていないようだ。
なので、次のようにするとClassLoaderを解決できる
@Parcelize class ExampleUser internal constructor( private val bundle: Bundle ) : Parcelable { init { // class loaderを指定して復元させる bundle.classLoader = javaClass.classLoader } }
- 実装はモノグサしたので、あとは
interface
なりabstract
なりで隠蔽してキレイに見せかけてあげれば手抜きで後方互換もある程度担保できるんじゃないかな