- だいたいのアプリはデータを保存する必要がある
onPause()
でユーザの進捗を保存する- アプリの設定を保存する
- ファイルやデータベースに巨大なデータを保存する
- 以下で見ていく内容:
- Shared preference ファイルに単純なデータ型の key-value ペアを保存する
- アンドロイドのファイルシステムでファイルを保存する
- SQLite によるデータベースの利用
- 比較的小さな key-value ペアでいい場合は SharedPreferences を使う
SharedPreferences
は key-value ペアを格納したファイルに対応し、それを読み書きするメソッドを提供- ファイルはフレームワークが管理する
- ファイルはプライベートにも共有にもできる
- getSharedPreferences(String, int): 複数の shared preference ファイルが必要なとき使う(第一引数で区別する)。
Context
から呼ぶ - getPreferences(int): ひとつの shared preference ファイルでいいときはこちらを使う。
Activity
から呼ぶ - Fragment から呼ぶ例:
Context context = getActivity();
SharedPreferences sharedPref = context.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE);
- Shared preference ファイルに名前をつけるときは自分のアプリだと分かる名前にしなければいけない(例:
"com.example.myapp.PREFERENCE_FILE_KEY"
) - Context.MODE_PRIVATE を Context.MODE_WORLD_READABLE とかにすることで他のアプリからも利用可能になる
- SharedPreferences.Editor を使用
- putXXX() し、commit() で保存
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(getString(R.string.saved_high_score), newHighScore);
editor.commit();
- getXXX() する
- デフォルト値の指定も可能
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
int defaultValue = getResources().getInteger(R.string.saved_high_score_default);
long highScore = sharedPref.getInt(getString(R.string.saved_high_score), defaultValue);
- Android には他のプラットフォームのディスクベースのものに似たファイルシステムがある
- File API を使用
- 大きなデータをシーケンシャルに読み書きする場合に適している
- 前提知識
- Linux のファイルシステム
- java.io API
- Android には内部ストレージと外部ストレージがある
- 昔はマイクロ SD カードなどが外部ストレージに相当したが、今は内蔵のストレージを内部と外部に分けているデバイスもある(なので外部ストレージだからといってリムーバブルだとは限らない)
- 内部ストレージ
- 常に利用可能
- 保存されたファイルはデフォルトでは自分のアプリによってのみアクセス可能
- アプリのアンインストール時、内部ストレージにあるアプリのファイルがすべて削除される
- ファイルをユーザにも他のアプリにも触られたくない場合に適している
- 外部ストレージ
- 常に利用可能というわけではない(外部ストレージを USB メモリとしてマウントしている時や外したとき)
- 誰でも読むことができる(world-readable)
- アプリのアンインストール時、getExternalFilesDir(String) で得られるディレクトリに置かれたファイルがすべて削除される
- ファイルのアクセス制御の必要がない時、他のアプリに共有したいとき、ユーザにアクセスさせたいときなどに適している
- android:installLocation で外部ストレージにアプリをインストールさせることもできる(通常は内部ストレージ)。参照: App Install Location
- 外部ストレージに書き込みを行うときはマニフェストファイルで WRITE_EXTERNAL_STORAGE を要求する必要がある
- 外部ストレージの読み込みは現在のところデフォルトで可能だが将来的に変わりうるので今のうちから READ_EXTERNAL_STORAGE を要求しておくとよい(WRITE_EXTERNAL_STORAGE を指定しているなら不要)
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
- 内部ストレージに保存するのにパーミッションの要求は不要
-
ディレクトリを取得する
- getFilesDir()
- アプリ用の内部ディレクトリを表わす File を取得する
- getCacheDir()
- アプリの一時キャッシュファイル用の内部ディレクトリを表わす File を取得する
- システムのストレージ容量が減ってくると警告なしに削除されるので適当にサイズ制限をするなど管理しておくこと
- getFilesDir()
-
例: File を作る
File file = new File(context.getFilesDir(), filename);
String filename = "myfile";
String string = "Hello world!";
FileOutputStream outputStream;
try {
outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
outputStream.write(string.getBytes());
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
- 例: File.createTempFile(String,String,File) でキャッシュファイルを作成
public File getTempFile(Context context, String url) {
File file;
try {
String fileName = Uri.parse(url).getLastPathSegment();
file = File.createTempFile(fileName, null, context.getCacheDir());
catch (IOException e) {
// Error while creating file
}
return file;
}
- Note: 内部ストレージディレクトリはアプリのパッケージ名で決まるので、内部ストレージのファイルのパーミッションを読み込み可にしてもディレクトリ名が分かっていないと他のアプリから読み込むことはできない
- 外部ストレージはアクセス前に領域が利用可能かどうか調べる必要がある
- Environment.getExternalStorageState() の結果が MEDIA_MOUNTED と等しければ読み書き可能
/* 外部ストレージが読み書き可能であることを調べる */
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
/* 外部ストレージが読み込み可能であることをチェック */
public boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
-
外部ストレージのファイルは 2 つに分けられる
- パブリック
- ユーザや他のアプリから自由に利用可能
- アプリのアンイストール時に削除されない
- アプリで撮った写真やダウンロードしたファイルなど
- プライベート
- アプリに所属するファイルで、アプリのアンイストール時に削除されるべきもの
- 技術的には外部から利用可能だが、現実的にそうしても意味がないようなもの
- アプリのダウンロードされた追加リソースとか、一次的なメディアファイルなど
- パブリック
-
外部ストレージにパブリックなファイルを保存したいときは、Environment.getExternalStoragePublicDirectory(String) を使用
- 第一引数には DIRECTORY_MUSIC や DIRECTORY_PICTURES など、保存したいファイルの種類を指定する
- 他のパブリックなファイルとあわせて整理できるように
- 第一引数には DIRECTORY_MUSIC や DIRECTORY_PICTURES など、保存したいファイルの種類を指定する
public File getAlbumStorageDir(String albumName) {
// Get the directory for the user's public pictures directory.
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
- 外部ストレージにプライベートなファイルを保存したいときは、Context#getExternalFilesDir(String) に作成したいディレクトリの名前を与えて呼び出す
- アプリの外部ストレージファイルを格納する親ディレクトリ(アプリのアンイストール時に削除される)の下にディレクトリが作られる
- (
DIRECTORY_PICTURES
など)定義済のディレクトリ名が用途に沿わない場合はnull
を与えることで親ディレクトリ自身を取得できる
public File getAlbumStorageDir(Context context, String albumName) {
// Get the directory for the app's private pictures directory.
File file = new File(context.getExternalFilesDir(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
- ディレクトリを作る際、パブリックであるかプライベートであるかに関わらず
DIRECTORY_PICTURES
などの定数を使うことが重要- これらの定数値を使うことで、システムに正しく扱われることが保証される
- 定義済の定数(抜粋)
DIRECTORY_ALARMS
DIRECTORY_DOWNLOADS
DIRECTORY_MOVEIS
DIRECTORY_MUSIC
DIRECTORY_NOTIFICATIONS
DIRECTORY_PICTURES
DIRECTORY_RINGTONES
- getFreeSpace() や getTotalSpace() を使うことで、(ファイル書き込み時の)例外に頼らず十分な空き領域があるかどうかを知ることができる
- getFreeSpace が返す分のデータ量を書き込めることを保証するものではない
- 書き込む分に加えて数 MB の余裕があるとか、使用量が 90% 未満であればおそらく大丈夫
- Note: どのくらいファイル容量が必要になるか分からないときなど、空き領域を問い合わせることは必ずしも必要ではない(そういう時はやはり例外を使う)
- 不要になったファイルは削除すること
- File#delete() を呼ぶ
- 内部ストレージにある場合は Context#deleteFile(String) でもよい
- SQL データベースの知識
- android.database.sql パッケージに API がある
-
スキーマに対応するコントラクト(contract)クラスというクラスを定義する
- コントラクトクラスは URI、テーブル、カラム名に対応する定数を持つクラス
- データベース全体にグローバルな定義をクラスのルートに置き、テーブルに対応する定義を内部クラスに持たせる
-
Note: BaseColumns インターフェースを実装しておくと cursor アダプタなどが期待する
_ID
というフィールドを継承することができる。必須ではないがこうしておくと Android フレームワークと協調できる -
例: あるテーブルのテーブル名とカラム名を定義する
public static abstract class FeedEntry implements BaseColumns {
public static final String TABLE_NAME = "entry";
public static final String COLUMN_NAME_ENTRY_ID = "entryid";
public static final String COLUMN_NAME_TITLE = "title";
public static final String COLUMN_NAME_SUBTITLE = "subtitle";
...
}
- 内部ストレージにファイルを保存したときと同様に、データベースはアプリに紐付いたプライベートな場所に保存される
- SQLiteOpenHelper クラス
- このクラスを使ってデータベースへの参照を取得すると、システムがデータベースの作成や更新など時間のかかりそうな処理を、アプリの起動時ではなく必要な時に行ってくれる
- getWritableDatabase() または getReadableDatabase() を呼ぶだけでよい
- Note: 長時間の処理になるおそれがあるので、これらのメソッドはバックグラウンドのスレッドで実行すること
- SQLiteOpenHelper を継承して、onCreate(SQLiteDatabase) と onUpgrade(SQLiteDatabase,int,int) と onOpen(SQLiteOpenHelper) をオーバーライドするクラスを作る(場合によっては onDowngrade(SQLiteOpenHelper,int,int) が必要になる場合もある)
public class FeedReaderDbHelper extends SQLiteOpenHelper {
// スキーマを変更した場合、このバージョンをインクリメントする
public static final int DATABASE_VERSION = 1;
public static final String DATABASE_NAME = "FeedReader.db";
public FeedReaderDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_ENTRIES);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// この例においては DB に入るデータはキャッシュなので全部消し去る
db.execSQL(SQL_DELETE_ENTRIES);
onCreate(db);
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
}
- いわゆるマイグレーション的なものを実現している?
- データベースにデータを挿入するには、ContentValues のインスタンスを insert(String,String,ContentValues) メソッドに渡す
- 第一引数はテーブル名、第二引数は …
nullColumnHack optional; may be null. SQL doesn't allow inserting a completely empty row without naming at least one column name. If your provided values is empty, no column names are known and an empty row can't be inserted. If not set to null, the nullColumnHack parameter provides the name of nullable column name to explicitly insert a NULL into in the case where your values is empty.
// Gets the data repository in write mode
SQLiteDatabase db = mDbHelper.getWritableDatabase();
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID, id);
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_CONTENT, content);
// Insert the new row, returning the primary key value of the new row
long newRowId;
newRowId = db.insert(
FeedReaderContract.FeedEntry.TABLE_NAME,
FeedReaderContract.FeedEntry.COLUMN_NAME_NULLABLE,
values);
- データの取得には query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) を使用する
- その結果は Cursor オブジェクト
SQLiteDatabase db = mDbHelper.getReadableDatabase();
// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
FeedReaderContract.FeedEntry._ID,
FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE,
FeedReaderContract.FeedEntry.COLUMN_NAME_UPDATED,
...
};
// How you want the results sorted in the resulting Cursor
String sortOrder =
FeedReaderContract.FeedEntry.COLUMN_NAME_UPDATED + " DESC";
Cursor c = db.query(
FeedReaderContract.FeedEntry.TABLE_NAME, // The table to query
projection, // The columns to return
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
sortOrder // The sort order
);
- Cursor オブジェクトの指す行を参照するには
- Cursor の移動メソッド(moveToFirst() など)と値の読み取りメソッド(getString(int), getLong(int) など)を使う
cursor.moveToFirst();
long itemId = cursor.getLong(
cursor.getColumnIndexOrThrow(FeedReaderContract.FeedEntry._ID)
);
// Define 'where' part of query.
String selection = FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
// Specify arguments in placeholder order.
String[] selectionArgs = { String.valueOf(rowId) };
// Issue SQL statement.
db.delete(table_name, selection, selectionArgs);
SQLiteDatabase db = mDbHelper.getReadableDatabase();
// New value for one column
ContentValues values = new ContentValues();
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE, title);
// Which row to update, based on the ID
String selection = FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
String[] selectionArgs = { String.valueOf(rowId) };
int count = db.update(
FeedReaderDbHelper.FeedEntry.TABLE_NAME,
values,
selection,
selectionArgs);