2010年3月16日火曜日

Android IO Utility

最近Androidを触る機会があり、いろいろと試してみている。

本当はAndroidもScalaで作りたいんだけど、色々と問題があるみたいなので今のところはあきらめてJavaで書いている。とはいえ、Eclipseのコード補完、リファクタリングはやはり便利で、フレームワークにプラグインするためのハンドラを書くことが中心のプログラムではScala+Emacsよりも便利である。Androidもこの範疇に入る。

Androidを触って感じたのは、全く組込みや制御系といった気がしないとうことである。体感的には普通のUNIX&Java&GUI&Webクライアントアプリを作っている手触り。

画面が小さいのとネットワークが遅いのとメモリが少ないのとキー入力がやりにくいのとGPSなどのデバイスがたくさんついているのが違うところ。でも、プログラミングモデルは普通のJavaでよい。昔の制御系みたいな職人芸は要らないから、アイデアしだいというところが、Androidがエンジニアに受けている理由のひとつだろう。もちろん、本格的なアプリケーションを作る段になれば、色々な技術が必要になるけれど、間口が広いというのはよいことである。

Androidは、通常ファイルの読み書きができるからAppEngineのプログラミングモデルと比べるとずいぶん楽である。ファイルをBLOB/CLOB的な使い方もできるし、JSONを使ってちょっとした情報を保存しておくこともできる。

モバイルアプリケーションなので、pause/resumeの状態遷移が発生した時の状態引継ぎが必須の作業になる。さらに、アプリケーションが強制終了されても、前回のセッションの状態に復元できることが望ましいので、その意味でもアプリケーションの状態保存のために手軽に使えるデータの保存場所が必要になる。この目的にJSONと通常ファイルの組み合わせが便利なのである。

また、ActivityやServiceといったエージェント間や同一エージェント内でも異なったスレッド間ではIPC的な通信になる。こういったエージェント間での情報受け渡しや情報文脈共有に不揮発性一時データを使う場合、RDBMSを使うのが本格的だけど、JSONをファイルに格納して受け渡すのが手軽で使いやすい。

もちろん、サーバーとの通信はJSONベースでよい。

以上の点から、AndroidではJSONによるデータの取り回しを前提にしたアーキテクチャにするのが筋がよさそうと感じたわけである。org.json.JSONObjectが基本で提供されているのも大きい。

JavaでXMLを使うのは少し煩雑だし、性能的にもいい所はないので、特別な要件がなければあまりXMLにこだわらないのがよいだろう。

そんなこともあり、AndroidIOUtilityというファイル入出力のユーティリティクラスを作った。

AndroidIOUtility.java
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;

import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;

public class AndroidIOUtility {
public static JSONObject loadJsonFromFile(String filename, Context context) \
    throws IOException, JSONException {
        return new JSONObject(loadStringFromFile(filename, context));
    }

public static void saveJsonFromFile(String filename, JSONObject json, \
    Context context) throws IOException, JSONException {
        OutputStream out = null;
        try {
            out = context.openFileOutput(filename, Context.MODE_PRIVATE);
OutputStreamWriter writer = new OutputStreamWriter(out, "utf-8");
            writer.append(json.toString(2));
            writer.flush();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {}
            }
        }
    }

public static String loadStringFromFile(String filename, Context context) \
    throws IOException {
        InputStream in = null;
        try {
            in = context.openFileInput(filename);
            InputStreamReader reader = new InputStreamReader(in, "utf-8");
            StringBuilder builder = new StringBuilder();
            char[] buf = new char[4096];
            int size;
            while ((size = reader.read(buf)) != -1) {
                builder.append(buf, 0, size);
            }
            return builder.toString();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {}
            }
        }
    }

public static void saveStringToFile(String string, String filename, Context \
    context) throws IOException {
        OutputStream out = null;
        Writer writer = null;
        try {
            out = context.openFileOutput(filename, Context.MODE_PRIVATE);
            writer = new OutputStreamWriter(out, "utf-8");
            writer.append(string);
            writer.flush();
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                } else if (out != null) {
                    out.close();
                }
            } catch (IOException ee) {}
        }
    }

public static void saveInputStreamToFile(InputStream in, String filename, \
    Context context) throws IOException {
        OutputStream out = null;
        try {
            out = context.openFileOutput(filename, Context.MODE_PRIVATE);
            byte[] buf = new byte[8192];
            int size;
            while ((size = in.read(buf)) != -1) {
                out.write(buf, 0, size);
            }
            out.flush();
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException ee) {}
        }
    }

public static void saveResourceToFile(int resourceId, String filename, \
    Context context) throws IOException {
        InputStream in = null;
        try {
            in = context.getResources().openRawResource(resourceId);
            saveInputStreamToFile(in, filename, context);
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException ee) {}
        }
    }

public static void truncateFile(String filename, Context context) throws \
    IOException {
        OutputStream out = null;
        try {
            out = context.openFileOutput(filename, Context.MODE_PRIVATE);
            out.write(new byte[0]); // XXX needs check
            out.flush();
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {}
            }
        }        
    }
}

内容は簡単なファイル入出力なので難しいところはない。それより、Android向けのアプリケーション・アーキテクチャを意識した機能セットという意味で見てもらえると参考になるかもしれない。

1つはJSONの入出力を基本に考えていること。理由は前述したとおり。

また、画像なども通常ファイルとしてキャッシュしたり受け渡ししたりすることになるので、生バイナリデータの入出力も必要になる。

saveResourceToFileメソッドは、リソースに格納した画像データをServiceへの受け渡し目的でファイルに書き出す必要があったので作った。リソースからのデータの取り出しも色々なパターンがありそうなので、目的に応じて用意するとよいだろう。

文字コードはUTF-8決め打ちにしている。もちろん、他の文字コードの読み書きが必要なケースも出てくるだろうけど、自アプリケーション内での通信や不揮発性一時データの保存に用途を絞ればUTF-8決め打ちが簡潔である。

アプリケーションデータはSDCardに置いておくのが作法のようなので、それむけのメソッドもいずれ作ることになると思う。ファイル入出力周りはGoogleのGuavaが便利そうなので、これを使うように改造してもよいかもしれない。

0 件のコメント:

コメントを投稿