飴屋

Androidアプリ/日記8

Froyo万歳!

SoftbankキャリアのHTC Desireも2010年10月8日にAndroid OS 2.2へのアップデートを果たしました。噂の通り、動作が全般的に機敏になったのが体感できました。おまけでキャリアメールのアプリも発表されましたが、特にGmailに不便を覚えないのでダウンロードだけして放置されてます。

FlashPlayerも10.1にアップデートされて、一通り動きが確認できました。マシンパワーのいるFlashはやはりちょっと動作が遅いですが、自分の作成した音声合成Flashがちゃんと発声しているのが聞けてちょっと嬉しかったです。先日FlashをCS5にアップグレードしましたし、iPhone側もFlashに対してちょびっと軟化した態度を見せましたし、スマートフォン向けのFlash開発はこの先ある程度の需要が見込めるのかもしれません。
Flashは過去の成果物をタッチインターフェイス用に書きなおせられれば、いろいろと楽しいこともできそうです。

GoogleがOracleに訴えられたり、Microsoftがスマートフォンのメーカーを訴えたり、いろいろありますが、Android端末の増加の勢いは衰え知らずだそうです。国内でもAUがついに動き出しました。(2010/10/11に書いてます)IT業界以外の友達にもiPhoneやXperiaを持ってる人が増えてきたので、自分でアプリを書いて使ってもらえると思うと開発のモチベーションも上がります。

さて、前回エミュレータで設計した画面が表示できるところまで確認できたので、実際にフォームに入力された体重の数値を記録していくプログラムのコーディングを始めようと思います。

データを保存してみよう

Androidアプリでデータを記録する場合には、いくつか手段があります。ファイルシステムにアクセスできるのでファイルベースのデータベースにしてもいいし、SharedPreferenceなんていうグローバルにアプリの状態を記録できるクラスも存在するそうです。そして、もう一つにSQLiteベースのRDBMSがあります。私もWEB系のシステム開発でSQLにはさんざんお世話になっているのでとてもとっつきやすそうですし、SQL文法のクエリを投げるだけでいろいろ集計やレコードの抽出ができるなんて、今回のアプリにうってつけだと思います。

ここでいろいろ調べていると「データの永続化」っていう言葉がでてきました。確か、mixiアプリを作ってるときにもそんな単語が出てきましたっけ。おそらくpersistenceという単語を日本語訳した結果だと思うのですが、なんか「未来永劫データを守り続ける」といった変なイメージが浮かんできて、自分は最初にこの単語を聞いたときに随分と首を捻りました。「データの保存」じゃ訳としてダメな理由が何かあるのかもしれませんね。

SQLiteの話

WEBサイトの開発では、レンタルサーバ会社の提供するMySQLやPostgreSQLのサーバにアクセスして、データベースを利用するという形態が多いのですが、Androidではデータベースサーバが端末内で動いているわけではないようです。メモリやCPUの希少性から常時駆動するサーバプログラムは忌避されているのかもしれません。その代わり、SQLiteというコマンドプログラムを呼び出して、データベースの編集を行うことができます。SQLiteはデータベースプログラムとしてはとてもシンプルな機能を有するのみですが、その分お手軽に利用できるというメリットがあるようです。携帯端末内で完結するアプリでは、トランザクションを気にする必要もほとんどないので機能的には必要十分だと思います。RDBMSとしての性能がどの程度なのか自分でもよくわかってませんが、今回の開発ではテーブルをまたいで行う処理もなさそうなので、リレーショナルさ加減は特に気になりません。一つ気になるのは、SQLiteデータベースにはアプリの内部からしかアクセスできないという点です。他のアプリからこの体重アプリのデータを使いたい場合は、また別のパイプを用意してあげる必要がでてきます・・・が、今のところそんな予定はないので気にしないでおきましょう。

最近はブラウザやメールクライアントにSQLiteベースのデータベースがついているものも増えてきました。クライアントツールにもデータベースが気軽についてくる時代なんでしょうか。

SQLiteは一つのデータベースを一つのファイルで構成するので、データベースのコピーやバックアップ、削除がファイルベースで行えるというのも特徴の一つかもしれません。

テーブルの設計

このアプリでどんなデータを使うかは既に設計済みだったかと思います。

  • 体重
  • 体脂肪率
  • 計測日時

この項目それぞれに名前をつけて、一つのテーブルに蓄積していきます。テーブルの名前は「weighttbl」とでも命名しましょう。今回のアプリでは、この体重が蓄積された「weighttbl」を「weightdb」という名前のデータベースに保存します。

  • weightdb
    • weighttbl
      • id - レコードID
      • weight - 体重
      • fat - 体脂肪率
      • dt - 計測日時

体重の数値は「4桁の整数値」とします。0-9999までの入力を受け付け、それぞれ0.0-999.9kgを表します。入力時には小数点の入力が面倒くさそうなので直接この4桁以下の数値を入力してもらうことにします。将来的に別の重さの単位での入力を受け付けるようになった場合も、「kg/10」単位に換算して記録することにしますので、四捨五入分の誤差が生じることになります。
「kg/10」って呼びにくいですね。一般的ではないですが「hg(ヘクトグラム)」とでも呼べばいいでしょうか。

体脂肪率の数値も「4桁の整数値」とし、0-1000までの入力を受け付け、それぞれ0.0-100.0%を表すことにします。千分率(パーミル)ってことですね。

計測日時は「日時」ですが、SQLiteには日時を表すデータ型がないらしいので、テキストか数値で表すことになります。「2010-10-11 14:42:00」みたいなテキストなら扱いなれているので、そんな感じにしようと思いますが、数値表現にした方がスピードやファイルサイズの節約になるのでしょうか。

レコードIDという項目が新たに加わっていますが、これはデータベースのレコードを識別するだけの存在です。これがあると同じ日時に同じ数値を記録してしまっても、IDをたよりにどちらか一方だけ削除するという操作も可能になります。CSVファイルからの重複インポートというのはありえる話だと思います。

データベース用のクラス

それでは、実際にプログラミングを開始してみます。DBというクラスを新たに作成して、クラス内に上で設計したデータベースを具体化していきます。内訳は以下の感じ。

  • DBクラス
    • DB名等の定数
    • レコードを表す内部クラス
    • データベース構築用クラス
    • データベース操作メソッド

順番に中身をみていきましょう。

DB名等の定数

public class DB {
public static final String DB_NAME = "weightdb";
public static final String DB_TABLE = "weighttbl";
private static final String[] COLS = new String[] {"id","weight","fat","dt"};
private SQLiteDatabase db;
private final DBOpener dbopener;
//省略
}

データベースやテーブルの名前は、プログラム中でしばしば使用するので、定数文字列を作っておくと後々便利だと思います。DB_NAMEがデータベース名、DB_TABLEがテーブル名です。さらに、テーブル内の各カラムの名前も配列としてCOLSに設定します。weighttblテーブルのカラムは4つです。

その下にDBクラスのプロパティを設定しています。
SQLiteDatabaseクラスはandroid SDKで提供されるSQLiteの機能の実体です。このインスタンスを使ってデータベースを操作することになります。
SBOpenerというクラスはまた後で説明しますが、データベースの構築を補助するクラスで、DBクラスの中で定義します。

レコードを表す内部クラス

データベースに格納する4つのカラムからなる各レコードをクラス表現します。
Weightクラスと定義し、各カラムのプロパティを持たせています。

public static class Weight {
public long id;
public int weight;
public int fat;
public Date dt;

public Weight(){}
public Weight(final long i,final int w,final int f,final String d) {
id = i;
setWeight(w);
setFat(f);
setDate(d);
}
public void setWeight(int w) {
if (w<0) w=0;
if (w>9999) w=9999;
weight = w;
}
public void setFat(int f) {
if (f<0) f=0;
if (f>1000) f=1000;
fat = f;
}
public String getDateTimeString() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
public void setDate(String d) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
dt= sdf.parse(d);
} catch (ParseException e) {
// error
}
}
}

コンストラクタが二つあり、空のインスタンスを作って後からプロパティを入力したり、プロパティ値を指定してインスタンスを作ったりできるようになっています。

プロパティは必要に応じてgetterやsetterを用意します。weight(体重)やfat(体脂肪率)には数値幅の制限があるのでsetWeight、setFatメソッドで入力値をチェックしています。またdt(計測日時)はString型で入力を受け付け、内部ではDate型で保持するようになっています。出力時もString型でSQLite向けの文字列を返すメソッド(getDateTimeString)を用意しました。「yyyy-MM-dd HH:mm:ss」というフォーマットが確か一般的だったと思います。

データベース構築用クラス

データベースの仕組みはAndroidが用意してくれますが、アプリ固有のテーブルの設置はアプリ内で行わなくてはなりません。weighttblテーブルが未設置の場合、新規にデータベースを作成するのを手伝ってくれるのが、「SQLiteOpenHelper」クラスです。これを継承してDBOpenerというクラスを作成しました。

private static class DBOpener extends SQLiteOpenHelper {
private static final String DB_CREATE = "CREATE TABLE " + DB.DB_TABLE
+ " (id INTEGER PRIMARY KEY, weight INTEGER NOT NULL, fat INTEGER NOT NULL, dt TEXT);";

public DBOpener(Context context) {
super(context,DB.DB_TABLE,null,3);
}
@Override
public void onCreate(final SQLiteDatabase db) {
try {
db.execSQL(DBOpener.DB_CREATE);
} catch (SQLException e) {
// error
}
}
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + DB.DB_TABLE);
onCreate(db);
}
}

DB_CREATEというのが、データベースを作成するSQL文を定義しています。CREATE TABLEで始まっている通り、それ以降の指示に従ってテーブルを作成しろという命令をしています。SQLiteではカラムのデータ型を指定する必要がないらしいのですが、指定してもいいそうです。

TEXT文字列
NUMERIC数値
INTEGER整数値
REAL実数値
NONE型指定なし

それで内部的には以下の型に判断されて保持されるらしいです。

NULL空値
INTEGER整数値
REAL浮動小数点値
TEXTテキスト
BLOBバイナリ値

SQLiteの仕様に不慣れなので一応書き出しみました。データの抽出とかでどうやってスピードを出すのか中身が気になりますね。

id、weight、fatは整数値なのでINTEGER型です。
dtは日付型がないので日付フォーマットの文字列をTEXT型で格納します。
idカラムには「PRIMARY KEY」という指定も行っていますが、レコードを識別するためのカラムであるというマークみたいなもんです。

SQLiteOpenHelperのメソッド「onCreate」「onUpgrade」をそれぞれ上書きしています。それぞれテーブルが作成されたり、再構成されたりするときに呼び出されるのでしょうね。再構築する際には「DROP TABLE」で始まるSQL文を発行しています。これは指定のテーブルを削除しろという命令です。テーブルを再構成する場合、以前のテーブルを一度削除して、それから新しいテーブルを作成しなおすわけです。必要であれば、テーブルのバックアップを取ったりもできるんじゃないでしょうか。

「アプリの利用」を時間軸で追うと、

  1. アプリを探す
  2. アプリをインストール
  3. アプリを初めて実行する
  4. アプリを終了する
  5. アプリを再起動する
  6. アプリを再終了する
  7. アプリをアップデートする
  8. アプリをアップデート後初めて実行する
  9. アプリをアンインストールする
  10. アプリを再インストールする
  11. アプリを再インストール後初めて実行する

といったいろいろな局面が想定されます。データベースの構築は「アプリを初めて実行する」タイミング以外にも、「アプリをアップデート後初めて実行する」や「アプリを再インストール後初めて実行する」といったタイミングでも必要になる工程なので、予めそこまで想定しておかないと問題が発生しそうです。

データベース操作メソッド

  • データベースを開く

まず、データベース操作用のクラスのコンストラクタです。

public DB(final Context context) {
this.dbopener = new DBOpener(context);
if (this.db == null) this.db = this.dbopener.getWritableDatabase();
}

先に定義したDBOpenerのインスタンス(dbopener)を保持し、SQLite機能の実体インスタンス(db)も取得しています。SQLiteOpenHelper.getWritableDatabaseメソッドが書き換え可能な状態でSQLiteデータベースを返してくれるようですね。とても便利です。

  • データベースを閉じる
public void cleanup() {
if (this.db != null) {
this.db.close();
this.db = null;
}
}

次にデータベースを閉じる操作を行うメソッドです。開いてたら閉じて、ガベージコレクションに回収させるというだけのシンプルなメソッドです。

  • データベースを更新する
public void insert(final Weight w) {
ContentValues values = new ContentValues();
values.put("weight", w.weight);
values.put("fat", w.fat);
values.put("dt", w.getDateTimeString());
this.db.insert(DB.DB_TABLE, null, values);
}

続いて、引数のWeightインスタンスを元にデータベースにレコードを追加する操作です。
ContentValuesというのが、挿入用のデータを格納するクラスのようですね。
SQLiteDatabase.insertメソッドにテーブル名と一緒に渡すとデータベースに記録されます。

public void update(final Weight w) {
ContentValues values = new ContentValues();
values.put("weight", w.weight);
values.put("fat", w.fat);
values.put("dt", w.getDateTimeString());
this.db.update(DB.DB_TABLE, values, "id=" + w.id, null);
}

次は既存のレコードを更新する操作です。insertと同様にテーブル名と更新データ(ContentValues)をSQLiteDatabase.updateに渡していますが、こちらには「どのレコードを更新するのか」という指示もついています。ここではidカラムを使ってレコードを特定していることになります。

public void delete(final long id) {
this.db.delete(DB.DB_TABLE, "id=" + id, null);
}

データベースを削除する場合はSQLiteDatabase.deleteメソッドを使います。
ここでもidカラムで削除対象を特定しています。

  • データベースを参照する
public Weight get(final long id) {
Cursor c = null;
Weight w = null;
try {
c = this.db.query(true, DB.DB_TABLE, DB.COLS, "id=" + id, null, null, null, null, null);
if (c.getCount() > 0) {
c.moveToFirst();
w = new Weight();
w.id = c.getLong(0);
w.setWeight(c.getInt(1));
w.setFat(c.getInt(2));
w.setDate(c.getString(3));
}
} catch (SQLException e) {
//error
} finally {
if (c != null && !c.isClosed()) {
c.close();
}
}
return w;
}

idカラムを指定して、データベースから情報を一件取得する操作です。
SQLiteDatabase.queryメソッドにテーブル名と取得対象レコードの指定しています。
取得したいカラム名の配列も指定できますので必要のないカラムを抜いて取得することもできますが、Weightクラスではすべてのカラムの情報が必要なのでDB.COLSを使って全てのカラムを取得するように指示してあります。

万が一、テーブル内のレコードでidが重複していて、複数のレコードが取得された場合、最初の一つが返ります。

また、指定のidのレコードがなかった場合は、nullが返るようになっています。

Cursorというクラスが出てきましたが、これはきっとデータベースから返却されたレコード間を走査するクラスですね。複数のレコードが返ってきた場合、順番に処理をするのに使えそうです。次のメソッドと一緒に再度説明します。

public List<Weight> getAll() {
ArrayList<Weight> ret = new ArrayList<Weight>();
Cursor c = null;
try {
c = this.db.query(DB.DB_TABLE, DB.COLS, null, null, null, null, null);
int numRows = c.getCount();
c.moveToFirst();
for (int i = 0; i < numRows; ++i) {
Weight w = new Weight();
w.id = c.getLong(0);
w.setWeight(c.getInt(1));
w.setFat(c.getInt(2));
w.setDate(c.getString(3));
ret.add(w);
c.moveToNext();
}
} catch (SQLException e) {
//error
} finally {
if (c != null && !c.isClosed()) {
c.close();
}
}
return ret;
}

最後にデータベース内のレコードを全件取得する操作です。前の一件取得する操作とやってることは同じですが、idを指定しないところが違います。idを指定しないということはすなわち、全てのレコードが取得条件に該当するということですね。

Cursorクラスのメソッドが8つ出てきています。

getCount取得できたレコードの数を返す
moveToFirst取得レコードの参照位置を先頭に移動する
moveToNext現在の参照位置を一つ次に移動する
getLong現在の参照位置の指定のカラムからlong値を取得する
getInt現在の参照位置の指定のカラムからint値を取得する
getString現在の参照位置の指定のカラムからString値を取得する
isClosedレコードの取得状況を返す
close取得されたレコードを破棄する

さて、全件取得するメソッドがあっても、使い道はほとんどないと思いますので、後でこれを「記録期間を指定して該当するものを全て取得する」メソッドに書きかえることになると思います。

実際にこのDBクラスを使ってデータを操作する部分はまた後ほど。

日記一覧