にんにんにん

エンジニアな日々を書いていきます

オブジェクトのやり取りにもの思う

Androidの話になりますが、例えばAPIからのレスポンスをJavaのentityとして保持していて、 Intentを飛ばして受け取る先でもそのentityを使いたい、という場面があるとします。

その際、考えられるのは

  1. Intentを受け取る先で再びAPIを叩く
  2. entityをIntentのpuExtra系メソッドでなんとか受け渡しする
  3. メモリキャッシュなど、キャッシュしておいてIntent受け取り先でそのキャッシュからentity復元

あたりかなと思います。(他にあったらごめんなさい)

ナンセンスなのは1です。値が必要なたびにAPIを叩いていたらロードでユーザを待たせることになりますし、APIサーバへのアクセスを頻繁にさせることで電池の消耗も激しくします。(Ream Academyにそんな話があった気が... Becoming a Better Battery Citizen

2のIntentで受け渡しするのはいくつか方法があります。

  • Parcelableとしてentityを実装
  • Gsonを使ってjson変換したものをStringとして受け渡し

ちなみに、うちのプロダクトコードではParcelableとして実装されたentityが受け渡されています。

うちのプロダクトのコードでよく見る例

public class Profile implements Parcelable {

    private int id;
    private String name;

    protected Profile(Parcel in) {

        id = in.readInt();
        name = in.readString(); 

    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(id);
        dest.writeString(name);
    }

    @SuppressWarnings("unused")
    public static final Parcelable.Creator<Profile> CREATOR = new Parcelable.Creator<Profile>() {
        @Override
        public CustomerProfile createFromParcel(Parcel in) {
            return new Profile(in);
        }

        @Override
        public Profile[] newArray(int size) {
            return new Profile[size];
        }
    }
}

entityとなるオブジェクトをParcelableとして実装しています。 Parcelableインタフェースを実装することで、そのオブジェクトをParcelとして扱うことができます。 (Parcelは世の中色々ありますが、Androidにおいてはこちらを参照 https://developer.android.com/reference/android/os/Parcel.html

Intentには、putExtra系のメソッドがあり、Intentにデータを渡すことができます。 その中に、putExtra(String name, Parcelable value) があり、オブジェクトを直接渡すことはできませんが、Parcelableとして実装したオブジェクトを渡すことができます。

しかし、これには難点があり、なんといっても実装が面倒臭い。(コードジェネレータ・プラグインアノテーションライブラリでなんとかする方法もあるが)

事実、この形のコードは今置き換えられているところです。

次に、Gson変換してStringとして受け渡しする方法もあります。

Gsonは、Googleの開発、保守しているライブラリで、シンプルなインタフェースでPOJOからJsonへのシリアライズ、またでシリアライズを行うことができます。 シリアライズされたJsonは、Stringとして扱うことができます。

public class Profile {

    private static final Gson GSON = new GsonBuilder().create();

    @SerializedName("user_id")
    private int id;

    @SerializedName("user_name")
    private String name;

    public static Profile fromJson(String json) {
        if (json == null) return null;
        return GSON.fromJson(json, Profile.class);
    }

    public String toJson() {
        return GSON.toJson(this);
    }
}

先ほどと比べてだいぶすっきりしました。 Intentを投げる側ではputExtra(String key, String value)で投げて、受け取り側は受け取った後でGson.fromJsonででシリアライズしてあげればいいだけです。

Intentのデータとして受け渡しする難点

しかし、Parcelableとして実装するにしろ、Gsonでシリアライズするにしろ、難点がいくつかあります。

  • アクティビティの結合が密になる。
  • オブジェクトのデータがでかくなると、クラッシュの危険が高くなる。

まず、必要なIntentのパラメータが増えるとアクティビティ感の結合が密になります。 例えば、MainActivityからProfileActivityに遷移する際、ProfileActivityが前のアクティビティからのProfileを期待している場合、 Profileを持っていないアクティビティからの遷移はできなくなります。 ProfileActivityの振る舞いは、遷移元のActivityに依存します。

また、オブジェクトがListで、アルバムなどの大量のデータを保持する場合、Intentはそんなに大きなデータを持つよう想定されていないため、クラッシュします。(実体験)

Intentのパラメータはuser_idなどのインデックスに留めておきたいものです。

キャッシュをしよう

そこで、 3. メモリキャッシュなど、キャッシュしておいてIntent受け取り先でそのキャッシュからentity復元 が今の所望ましい形なのかなと考えています。

アーキテクチャとしてrepositoryを用意し、Activityは何も考えずにrepositoryからデータを取ってくるメソッドを叩く。 repositoryはキャッシュの有無に応じて、RemotoDataSourceから取ってくるか、LocalDataSourceからとってくるか選択する。 repositoryのクライアントはデータの出どころは知る必要なく、データを取得することができる。

この方法がアクティビティ間の結合を疎にできて、ユーザがロード時間にストレスを感じなくて済み、開発者は実装に時間を取られずに済む方法かな?と最近は思っています。

実際、プロダクトにまだrepositoryを完全に導入できていなくて、ParcelableになっているところをGsonで簡素化してごまかしている節があるので、早いところ整理したいものです。