View on GitHub
mixi-inc/AndroidTraining
直列化とコレクション、永続化
この章では、Android 向けの直列化とコレクション、永続化について解説します。

参考 : Bundle | Android Developers
参考 : SparseArray | Android Developers
参考 : Serializable | Android Developers
参考 : Parcelable | Android Developers
参考 : Parcel | Android Developers
参考 : JSONObject | Android Developers
参考 : JSONArray | Android Developers
参考 : Storage Options | Android Developers
参考 : SharedPreferences | Android Developers
参考 : Context#getExternalFilesDir() | Android Developers

目次

コレクション

Java には Collection インタフェースを実装したコレクションクラスが、コレクションフレームワークとして提供されています。
この、コレクションフレームワークで提供されている各クラスは、データセットの管理をする上で非常に重要な役割を持っています。

この項目では、Android に最適化されたコレクションフレームワークのうち、Bundle、SparseArrayについて説明をします。

Bundle

Bundle は、String をキーとして、各種の Android 向けに最適化されたオブジェクト(各種プリミティブ型、Serializable 型、Parcelable 型、これらの配列ないし List コレクション)をマッピングするためのコレクションです。
Intent の Extras は、内部では Bundle オブジェクトとして管理しています。

Bundle オブジェクトにマッピングを追加するメソッドは put*() です。
マッピングに追加する型に合わせてメソッドが用意されています。

これに対応して、マッピングから値を取り出すメソッドが、get*() として定義されています。
追加した時と取り出す時の型は一致している必要があります。

呼び出し元のActivity

    public void send(View v, int value) {
        Intent intent = new Intent(this, ResultActivity.class);
        Bundle bundle = new Bundle();
        bundle.putString("data","value");
        intent.putExtra("bundlePrams", parcelableData);
        startActivity(intent);
    }

呼び出し先のActivity

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Bundle extras = getIntent().getExtras();
        Bundle bundlePrams = extras.getParcelable("bundlePrams");
        Toast.makeText(this, "data:" + bundlePrams.getString("data"), Toast.LENGTH_LONG).show();
    }

SparseArray

SparseArray は、int 型の値を key とした HashMap のようなものです。
key を int 型に固定することにより、HashMap よりも高速に動作します。

SparseArray では、int 型の key の値は、連続している必要がありません。この点が、Sparse という名前の所以です。

Android では、リソースへのアクセスのために R.id.hoge など int 型の ID を用いることが多いため、それに対するマッピングを行いたいケースは多くなります。HashMapを利用する際にはSparseArrayが利用できないかを検討してみてください(Android Lint が警告を出してくれます)。

また、SparseArray では、値の型は自由な型が指定出来ますが、value が boolean または int の場合には、SparseBooleanArraySparseIntArray が利用可能です。

int 型では key の空間が足りない場合は、key に long 型を利用した LongSparseArray も存在します。

SparseArray<String> array = new SparseArray<String>();
array.add(R.id.TextView1, "hoge");
array.add(R.id.TextView2, "fuga");
array.add(R.id.TextView3, "foo");
array.add(R.id.TextView4, "bar");

直列化

この項目では、Java で提供されている直列化フレームワークと、Android で提供されている直列化フレームワークの両方を解説します。

Serializable

Java で提供されている直列化フレームワークで、オブジェクトの保存と復旧の方法を決めるためのインタフェースです。
ObjectInputStreamObjectOutputStreamの I/O の仕組みを利用して、オブジェクトを何らかの形式で以ってデータ化(シリアライズ)可能であること、またそのデータ化したものからの復旧(デシリアライズ)が可能であることを保証します。
実際にどのような形式で直列化を行うかは、個々の実装に依存します。

public class MyObject implements Serializable {
    public static final long serialVersionUID = -4324129709521L;
    private String mName;
}

Serializable インタフェースには、メソッドの宣言がありません(このようなインタフェースの事をマーカインタフェースと呼ぶ)。

直列化のフレームワークで扱うものは、クラスのフィールドです。特に、フィールド名とそのフィールドが持つデータについてを扱います。
ですので、staticな変数や定数、transientな変数、メソッドは直列化されません。
また直列化のフレームワークでは、一定のフォーマットへの変換を想定するため、クラスのフィールドの互換性を管理する必要があります。
互換性のないクラスや、フィールドに変更が加えられたクラスでは、直列化のフレームワークが上手く機能しなくなることがあります。この場合、自前で直列化の実際の処理を記述する必要が有ることに注意してください。

特に理由がなければ、以降で述べる Parcelable や JSONObject などより軽量なフレームワークの利用を推奨します。

Parcel と Parcelable

Android で提供されている直列化フレームワークです。
ただし、Serializable と異なり、永続化の為のフレームワークではありません。
プロセス間通信でハイパフォーマンスを得るために設計されたインタフェースです。Intent や Bundle も Parcelable を実装したクラスになっています。

Parcelable のインタフェースに書き込み処理と復元(読み込み)処理を定義することによりメッセージングで型を限定されずオブジェクトを渡すことが出来るようになっています。
このことにより、Intent で呼び出す Activity などに対してパラメータとしてプリミティブ型や String 型ではなく、独自に定義した型を渡したい場合に利用することができます。

Parcel、Parcelable は次のような特徴を持っています。

  1. Parcelable の実装クラスの中に Parcelable ではないクラスを含めることが可能
  2. 異なるアプリケーションへのメッセージングでも同一オブジェクトが復元できることが保証される

Parcelable を実装するには、下記の手順が必要です。

  1. Parcelable#writeToParcel() メソッド内でParcelに対してデータを書き込む
  2. CREATORという定数を定義し、Parcelable.Creator<T>を実装
  3. Parcelable.Creator#createFromParcel()Parcelからオブジェクトを生成する処理を実装

Parcel から読み込む順序はParcelable#writeToParcel()で書き込んだ順序と同じになるようにする必要があります。

Parcelable を Intent で受け渡しするサンプルは下記のようになります。

受け渡し対象のParcelableを実装したクラス

public class MyParcelable implements Parcelable {
    public static final Parcelable.Creator<MyParcelable> CREATOR = new Parcelable.Creator<MyParcelable>() {
        public MyParcelable createFromParcel(Parcel in) {
            return new MyParcelable(in);
        }

        public MyParcelable[] newArray(int size) {
            return new MyParcelable[size];
        }
    };
    private int mData;

    public MyParcelable() {}

    private MyParcelable(Parcel in) {
        mData = in.readInt();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(mData);
    }

    public void setData(int data) {
        mData = data;
    }
    public int getData(){
        return mData;
    }
}

呼び出し元のActivity

    public void send(View v, int value) {
        Intent intent = new Intent(this, ResultActivity.class);
        MyParcelable parcelableData = new MyParcelable();
        parcelableData.setData(value);
        intent.putExtra("parcelableData", parcelableData);
        startActivity(intent);
    }

呼び出し先のActivity

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Bundle extras = getIntent().getExtras();
        MyParcelable parcelData = extras.getParcelable("parcelableData");
        Toast.makeText(this, "data:" + parcelData.getData(), Toast.LENGTH_LONG).show();
    }

JSONObject と JSONArray

オブジェクトのシリアライズ・デシリアライズ形式の 1 つである JSON 形式を取り扱うためのフレームワークです。 JSONRPC など主にネットワーク経由でのデータの転送に利用します。

JSON におけるオブジェクトに対応するものがJSONObject、JSON におけるオブジェクトの配列に対応するものがJSONArrayとなります。
このため、JSONObject と JSONArray を正しく使い分ける必要があります。
例えば、下記のような場合はJSONExceptionとなります。

JSONObject obj = new JSONObject("[\"value1\",\"value2\",\"value3\"]");

JSONObject から値を取得する方法として、二種類の方法が提供されています。

1 つは、JSONObject#get()のような、getを接頭辞とするメソッドを用いて取得する方法です。
それぞれ、取得する型に合わせて、JSONObject#getString()JSONObject#getInt()など、型を指定したものを使います。

JSON 文字列上の型と異なる型を取得するメソッドを用いた場合、適宜型が変換されることと、存在しない key を指定した場合にJSONExceptionがスローされることに注意してください。

2 つには、JSONObject#opt()のような、optを接頭辞とするメソッドを用いて取得する方法です。
こちらも、それぞれ取得する型に合わせてJSONObject#optString()JSONObject#optInt()などのメソッドを利用します。

こちらは、存在しない key を指定した場合には、フォールバックの値が使用されます。

いずれの場合においても、下記のような JSON をJSONObject#getString()JSONObject#optString()でパースする場合、null値ではなく、文字列として”null”が帰ってくることに注意が必要です。

{
    "hoge": null
}

値がnull値かどうかを判定するメソッドとして、JSONObject#isNull()があります。

JSON を取り扱うその他のライブラリ

JSONObject や JSONArray 以外にも、JSON を取り扱う為のライブラリがあります。

その 1 つとして、Gson という、JSON 文字列から Java のオブジェクトにマッピングするフレームワークがあります。
このライブラリでは、自分で JSON 文字列から値を取り出す処理を書かなくても、JSON 文字列でのデータ構造と、Java のクラスのデータ構造を一致させておくことで、自動で JSON 文字列から Java のオブジェクトを生成してくれます。

ただし、特定の端末でGsonライブラリを利用するとVerifyErrorでクラッシュしてしまうので、対象の端末でも利用可能にするためには、名前空間の変更が必要となります。
利用する場合は作成するアプリケーションのサポート端末に注意してください。

永続化

Android では永続化の手法がいくつか用意されています。

  • Shared Preferences
  • Internal Storage
  • External Storage
  • SQLite Databases
  • Network Connection

この章では Shared Preferences、Internal Storage、External Storageについて扱います。
SQLite Databases についてはデータベースの章で扱います。
Network Connection についてはネットワークを経由してサーバー上にデータを保存することを指しています。ネットワーク通信についてはネットワーク通信の章で扱います。

SharedPreferences

SharedPreferences は Android 標準で用意されている永続化の方法の一つです。
key と value の組み合わせで、プリミティブ型やString型のデータを永続化します。

ファイルの読み書きと、ファイルの状態の管理・監視を請け負っているため、自前でファイル I/O を準備するよりも簡単にデータストアの仕組みが実装できます。

ファイルの実体

SharedPreferences を用いた場合、ファイルそのものは、アプリの持つ内部ストレージ領域に保存されます。このため、アプリのアンインストール時には、ファイルも含めて削除されることになります。

このため、アプリケーションが再インストールされてもなおデータを永続化したいユースケースには適用できません。
その場合でもデータを保持しておきたい場合は、適宜サーバにデータを保存するようにするのが良いでしょう。

一部端末において、保存場所が違うことに起因する問題が報告されています。
対象の端末をサポートする場合、Android-Device-Compatibilityを利用するなどして問題を回避することを推奨します。

ファイルへのアクセス権限

SharedPreferences のファイル作成時に、モードと呼ばれるアクセス権限を設定します。

通常、アプリの領域内へは他のアプリがアクセス出来ない(MODE_PRIVATE)ようになっていますが、これを SharedPreferences のファイル単位でコントロールが可能です。

ただし、SharedPreferences はアプリの各種設定項目を保存する目的でも使用されるため、他のアプリからのアクセスを許可することは推奨されません。

代わりに、アプリ間連携のための仕組みを使用してください。

また、複数のプロセスをもつアプリケーションで同じファイルを参照する場合に利用する MODE_MULTI_PROCESS というモードも存在しますが、サポートされるのは API Level 11(Android3.0) 以降なので、2.x 系の端末をサポートする場合には利用できません。

SharedPreferences の使用

SharedPreferences のインスタンスはContext#getSharedPreferences(String, int)で取得します。
第 1 引数には扱いたい SharedPreferences 名を指定します。ここで指定した名前がそのままファイル名となります。
第 2 引数には SharedPreferences のモードを指定します。
モードは対象のSharedPreferencesに対するアクセス権の設定です。デフォルトではMODE_PRIVATEになります。

データの保存

データの保存は、SharedPreferences.Editor クラスを介して行います。

  1. SharedPreferences#edit()SharedPreferences.Editorオブジェクトを取得
  2. SharedPreferences.Editor#putString(String,String)などで値を設定する
  3. SharedPreferences.Editor#commit()またはSharedPreferences.Editor#apply()で保存する

SharedPreferences.Editor#apply()は保存の完了を待たずに処理が戻ってきます。
そのため、保存に成功したかを確認することができません。 また、apply()は API Level 9 (Android 2.3) 以降のサポートになるため、Android2.2 以前の端末をサポートする場合利用できません。

メインスレッドから SharedPreferences を扱っていて、保存の成功、失敗を意識しなくていい場合以外は、非同期処理の中でSharedPreferences.Editor#commit()を使用することが望ましいでしょう。

値を削除する場合はSharedPreferences.Editor#remove(String)で個別に削除できます。
全ての値を削除したい場合はSharedPreferences.Editor#clear()メソッドを用います。

値の削除に関してもSharedPreferences.Editor#commit()ないしSharedPreferences.Editor#apply()を呼び出すまでは保存されません。

    // データの保存
    public boolean savePerson(String name, int age) {
        SharedPreferences sp = getSharedPreferences("person", MODE_PRIVATE);
        Editor editor = sp.edit();
        editor.putString("name", name);
        editor.putInt("age", age);
        return editor.commit();
    }

データの取得

データの取得には、SharedPreferences#getString()SharedPreferences#getBoolean()SharedPreferences#getFloat()SharedPreferences#getLong()SharedPreferences#getStringSet()など、それぞれの型に合わせたメソッドを使います。

保存時の型と取得時の型は一致している必要があります。

    // データの取得
    private String mName;
    private int mAge;
    public void readPerson() {
        SharedPreferences sp = getContext().getSharedPreferences("person", MODE_PRIVATE);
        mName = sp.getString("name", "no name");
        mAge = sp.getInt("age", 0);
    }

データ変更の監視

データ変更の監視のためのオブジェクトとして、SharedPreferences.OnSharedPreferenceChangeListenerが定義されています。

このリスナオブジェクトを、SharedPreferences#registerOnSharedPreferenceChangeListenerメソッドで登録することで、SharedPreferences の中で管理しているデータに変更が発生した際に、コールバックを受けることが出来るようになります。

保存されている値に変更があった場合に、その変更を画面上に反映するといった場合に利用されます。

登録した SharedPreferences.OnSharedPreferenceChangeListener は、Context のライフサイクルの終わりに必ずSharedPreferences#unregisterOnSharedPreferenceChangeListener()で登録解除をしてください。
登録解除をしなかった場合は、対象のオブジェクトに参照が残ってしまうためリークが発生するので注意が必要です。

public class MainActivity extends Activity implements OnSharedPreferenceChangeListener { 
    private SharedPreferences mSharedPreferences;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mSharedPreferences = getSharedPreferences("sample", MODE_PRIVATE);
        mSharedPreferences.registerOnSharedPreferenceChangeListener(this);
    }
    protected void onDestroy(){
        mSharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
        super.onDestroy();
    }

    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        String value = sharedPreferences.getString(key, null);
        if (value != null) {
            TextView tv = (TextView) findViewById(R.id.PreferencesValue);
            tv.setText(value);
        }
    }
}

Internal Storage

Internal Storage はその名の通りデバイスの内部ストレージのことです。

内部ストレージに保存されたファイルは SharedPreferences と同様、モードによってアクセス制御をすることができます。
これにより安全にファイルを保存することができます。ただし、内部ストレージは外部ストレージに比べて容量に制限があることが多いため、画像ファイルなどを大量に保存することには向きません。

標準で、Context は内部ストレージを利用するためのインタフェースを提供しています。

  1. Context#openFileOutput()で、FileOutputStreamを取得する
  2. FileOutputStream#write()で保存したいバイト列をストリームに格納する
  3. FileOutputStream#close()でストリームを閉じる

Java のファイル入出力の仕組みに倣い、FileOutputStream#close() はエラーが発生した場合でも必ず呼び出されるようにする必要があります。

String FILENAME = "hello_file";
String string = "hello world!";

FileOutputStream fos = null;
try {
  openFileOutput(FILENAME, MODE_PRIVATE);
  fos.write(string.getBytes());
} catch (IOException e) {
  // 例外処理
} finally {
  try {
    if (fos != null) {
      fos.close();
    }
  } catch (IOException e) {

  }
}

ファイルの内容を取得する場合は

  1. Context#openFileInput()でファイル名を指定しFileInputStreamを取得します。
  2. FileInputStream#read()でファイル内容を読み出します。
  3. FileInputStream#close()でストリームを閉じます。

保存時同様、FileInputStream#close()は必ず呼び出されるようにしてください。

Cache Directory

一時ファイルとしてキャッシュしたいファイルを扱うための特別なディレクトリです。
内蔵ディレクトリに、キャッシュ用のディレクトリが作成されます。

External Storage

External Storage は外部ストレージのことです。おおよそ SD カードがこれに当たりますが、端末によっては、内部ストレージとは別の内蔵メモリに保存している場合があります (Galaxy Nexus 等)。

外部ストレージはアクセス制御をすることができません。
そのため、全てのアプリケーションからアクセスすることができるので、保存する内容には注意が必要です。

外部ストレージを取得するには getExternalFilesDir() で外部ストレージのルートディレクトリを取得出来ます。
また、引数の type で DIRECTORY_MUSIC, DIRECTORY_PODCASTS, DIRECTORY_RINGTONES, DIRECTORY_ALARMS, DIRECTORY_NOTIFICATIONS, DIRECTORY_PICTURES, DIRECTORY_MOVIES を指定することにより、用途別のディレクトリを取得ができます。

実習・課題

プロジェクトの開き方は課題プロジェクトの開き方を参照してください。

実習

この実習では 1-7-SerializeAndCollectionPractice を使用します。

コレクション・直列化

  1. SerializableActivity で JSONの文字列をパースし、画面に表示してください。

永続化

  1. SharedPreferencesActivity で SharedPreferencesに値を保存、取得をしてください。
    アプリケーションを終了しても保存した値を取得できることの確認をしてください。 また、保存した値を削除してください。
  2. StorageActivity で内部ストレージ、外部ストレージにそれぞれファイルを保存してください。
    内部ストレージにはテキストファイル、外部ストレージには画像ファイルを保存してください。 外部ストレージに保存されたファイルをファイラーなどで参照してみてください。

課題

この課題では 1-7-SerializeAndCollectionAssignment を使用します。

コレクション・直列化

  1. SerializableListActivityでJSONのレスポンスをパースし、一覧表示してください。
    また、ListView部分をタップすると、詳細画面(SerializableDetailActivity)を表示するようにしてください。
    その際、表示するデータはすべてSerializableListActivityから渡すようにしてください。

永続化

  1. SharedPreferencesActivityでSharedPreferencesを利用してアプリケーションを終了しても保存されるカウンターを作成してください。
    また、そのカウントを画面上に表示してください。その際、onSharedPreferenceChanged内から変更を行なってください。