Android におけるデータ保存
[履歴] [最終更新] (2017/08/13 19:41:27)
ここは
趣味のプログラミングを楽しむための情報共有サービス。記事の一部は有料設定にして公開できます。 詳しくはこちらをクリック📝
最近の投稿
注目の記事

概要

Android におけるデータ保存の方法は主に三つ用意されています。それぞれの利用方法をまとめます。

関連する公式ドキュメント

環境設定値として保存 (Shared Preferences)

環境設定を簡易 KVS として利用できます。ファイルに保存されるため、アプリケーションが kill されても値は消失しません。

以下のサンプルでは Shared Preferences を直接利用していますが、そうではなくこれを内部的に利用して、アプリケーション設定の UI を構築するための仕組みが存在します。簡易 KVS ではなくアプリケーションの設定をユーザーが管理する UI を作成するためには後者を利用すると簡単です。

package com.example.mycompany.myapp;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static final String PREF_FILE_NAME = "com.example.mycompany.myapp.PREF_FILE_NAME";

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

        // 設定ファイルを開きます。
        SharedPreferences sharedPref = getSharedPreferences(PREF_FILE_NAME, Context.MODE_PRIVATE);

        // 値の取得
        int intVal = sharedPref.getInt("MY_KEY", 123); // 既定値 123 を設定
        Log.d(TAG, "MY_VALUE: " + intVal);

        // 値の設定 (起動する毎に値が +1 されます)
        SharedPreferences.Editor editor = sharedPref.edit();
        editor.putInt("MY_KEY", intVal + 1);
        editor.commit();
    }
}

実行例

08-12 11:49:41.905 5893-5893/com.example.mycompany.myapp D/MainActivity: MY_VALUE: 123
↓終了して再度起動
08-12 11:50:13.073 5893-5893/com.example.mycompany.myapp D/MainActivity: MY_VALUE: 124
↓終了して再度起動
08-12 11:50:28.786 5893-5893/com.example.mycompany.myapp D/MainActivity: MY_VALUE: 125

ファイル保存

Android ファイルシステムには、常に利用可能であることが保証されておりアプリケーション外からアクセスすることを基本的に想定しない Internal なものと、SD カード等によって提供される常に利用できるとは限らず、他のアプリケーションやユーザーからも参照される可能性のある External なものがあります。以下のサンプルは Internal なファイルシステムに対して読み書きしています。External なファイルシステムに対して操作を行う際は、利用可能かどうかの事前判定や AndroidManifest.xml におけるユーザーへの Permission 許可依頼が別途必要です。

package com.example.mycompany.myapp;

import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

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

        // 書き込み
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new OutputStreamWriter(openFileOutput("my_file.txt", Context.MODE_PRIVATE)));
            writer.write("abc\n123");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if(writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // 読み出し
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(openFileInput("my_file.txt")));
            String str = null;
            while((str = reader.readLine()) != null) {
                Log.d(TAG, str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if(reader != null) {
                    reader.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // Internal ファイルシステムにおける保存場所
        Log.d(TAG, "saved dir: " + getFilesDir());

        // 保存されているファイルリスト
        String[] files = fileList();
        for(int i = 0; i < files.length; ++i) {
            Log.d(TAG, "found: " + files[i]);
            deleteFile(files[i]); // 削除
        }
    }
}

Java 7 相当以上の API レベルであれば try-with-resources 構文を利用した記述が可能です。また、Android Studio におけるファイルエクスプローラは「Tools → Android → Android Device Monitor → File Explorer」で利用できます。

実行例

08-12 19:17:03.878 5837-5837/com.example.mycompany.myapp D/MainActivity: abc
08-12 19:17:03.878 5837-5837/com.example.mycompany.myapp D/MainActivity: 123
08-12 19:17:03.879 5837-5837/com.example.mycompany.myapp D/MainActivity: saved dir: /data/user/0/com.example.mycompany.myapp/files
08-12 19:17:03.879 5837-5837/com.example.mycompany.myapp D/MainActivity: found: my_file.txt

SQLite DB 保存

Android では Rails でも既定の設定で利用されている SQLite が組み込み DB として利用できます。上述「ファイル保存」における Internal ファイルシステムと同様、保存された情報は他のアプリケーションから参照できません。以下のサンプルでは簡単のため main スレッドで DB 操作していますが、一般に時間のかかる処理であり実際のアプリケーション開発時には IntentService などで別スレッドを用意するようにします。

MyDbContract.java

contract class とよばれるクラスを用意し、そのインナークラスで各テーブルを定義します。

package com.example.mycompany.myapp;

import android.provider.BaseColumns;

public final class MyDbContract {
    // コンストラクタを利用できないようにします。
    private MyDbContract() {}

    // テーブル名、列名の定数定義です。
    public static class MyTable implements BaseColumns {
        public static final String TABLE_NAME = "my_table";
        public static final String COLUMN_NAME_INT_COL = "int_col";
        public static final String COLUMN_NAME_STR_COL = "str_col";
    }
}

MyDbHelper.java

後に MainActivity で DB 操作のために利用するクラスです。先程定義した contract class 内の MyTable を static インポートしています。

package com.example.mycompany.myapp;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import static com.example.mycompany.myapp.MyDbContract.MyTable;

public class MyDbHelper extends SQLiteOpenHelper {

    // スキーマに変更があれば VERSION をインクリメントします。
    public static final int DATABASE_VERSION = 1;

    // SQLite ファイル名を指定します。
    public static final String DATABASE_NAME = "MyDb.db";

    // SQLite ファイルが存在しない場合や VERSION が変更された際に実行する SQL を定義します。
    private static final String SQL_CREATE_TABLE =
            "CREATE TABLE " + MyTable.TABLE_NAME + " (" +
                    MyTable._ID + " INTEGER PRIMARY KEY," +
                    MyTable.COLUMN_NAME_INT_COL + " INTEGER," +
                    MyTable.COLUMN_NAME_STR_COL + " TEXT)";

    private static final String SQL_DROP_TABLE = "DROP TABLE IF EXISTS " + MyTable.TABLE_NAME;

    public MyDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) { // SQLite ファイルが存在しない場合
        db.execSQL(SQL_CREATE_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // VERSION が上がった場合に実行されます。本サンプルでは単純に DROP して CREATE し直します。
        db.execSQL(SQL_DROP_TABLE);
        onCreate(db);
    }

    @Override
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // VERSION が下がった場合に実行されます。本サンプルでは単純に DROP して CREATE し直します。
        onUpgrade(db, oldVersion, newVersion);
    }
}

MainActivity.java

簡単のためすべて onCreate() 内でクエリを実行しています。実際にはバックグラウンドで別スレッドで処理すべきです。また、コネクションを張る処理は重いため onDestroy() で閉じるまで使い回しています。

package com.example.mycompany.myapp;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

import static com.example.mycompany.myapp.MyDbContract.MyTable;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    // DB を操作するためのインスタンス
    private MyDbHelper mDbHelper = null;

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

        mDbHelper = new MyDbHelper(getApplicationContext());
        SQLiteDatabase reader = mDbHelper.getReadableDatabase();
        SQLiteDatabase writer = mDbHelper.getWritableDatabase();

        // INSERT
        ContentValues values = new ContentValues();
        values.put(MyTable.COLUMN_NAME_INT_COL, 123);
        values.put(MyTable.COLUMN_NAME_STR_COL, "aaa");
        writer.insert(MyTable.TABLE_NAME, null, values);

        // SELECT
        String[] projection = { // SELECT する列
                MyTable._ID,
                MyTable.COLUMN_NAME_INT_COL,
                MyTable.COLUMN_NAME_STR_COL
        };
        String selection = MyTable.COLUMN_NAME_INT_COL + " = ?"; // WHERE 句
        String[] selectionArgs = { "123" };
        String sortOrder = MyTable.COLUMN_NAME_STR_COL + " DESC"; // ORDER 句
        Cursor cursor = reader.query(
                MyTable.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
        );
        while(cursor.moveToNext()) {
            long id = cursor.getLong(cursor.getColumnIndexOrThrow(MyTable._ID));
            String str = cursor.getString(cursor.getColumnIndexOrThrow(MyTable.COLUMN_NAME_STR_COL));
            Log.d(TAG, "id: " + String.valueOf(id) + ", str: " + str);
        }
        cursor.close();

        // DELETE
        String deleteSelection = MyTable._ID + " > ?"; // WHERE 句
        String[] deleteSelectionArgs = { "5" };
        writer.delete(MyTable.TABLE_NAME, deleteSelection, deleteSelectionArgs);

        // UPDATE
        ContentValues updateValues = new ContentValues();
        updateValues.put(MyTable.COLUMN_NAME_STR_COL, "bbb");
        String updateSelection = MyTable.COLUMN_NAME_STR_COL + " = ?";
        String[] updateSelectionArgs = { "aaa" };
        writer.update(
            MyTable.TABLE_NAME,
            updateValues,
            updateSelection,
            updateSelectionArgs);
    }

    @Override
    protected void onDestroy() {
        if(mDbHelper != null) {
            mDbHelper.close(); // コネクションを閉じます。
        }
        super.onDestroy();
    }
}

adb コマンドで SQLite に接続してデバッグ

Uploaded Image

Android Studio で SDK と同封されてインストールされる adb (Android Debug Bridge) コマンドを利用すると Android 端末に shell で接続できます。「Preferences → Appearance & Behavior → System Settings → Android SDK → Android SDK Location」で SDK の場所を確認して PATH を通しておきます。

$ /Users/username/Library/Android/sdk/platform-tools/adb shell
generic_x86:/ $ su
generic_x86:/ #

SQLite DB ファイルの場所を探します。

generic_x86:/ # find . -name 'MyDb.db'
...
./data/data/com.example.mycompany.myapp/databases/MyDb.db

sqlite3 コマンドで接続します。

generic_x86:/ # sqlite3 ./data/data/com.example.mycompany.myapp/databases/MyDb.db

テーブル一覧

sqlite> .tables
android_metadata  my_table

スキーマ確認

sqlite> .schema my_table
CREATE TABLE my_table (_id INTEGER PRIMARY KEY,int_col INTEGER,str_col TEXT);

SQL 発行

sqlite> select * from my_table;
1|123|bbb
2|123|bbb
3|123|bbb
4|123|bbb
5|123|bbb
関連ページ
    概要 電子工作や製品のプロトタイピング (例『地球規模で遠隔操作できるブルドーザー』) で利用される Raspberry Pi 3 について、こちらのページで構築した環境で Android Things アプリケーションを開発できます。 本ページでは、簡単な例として LED を点灯させるアプリケーションを扱います。より実用的なアプリケーションを開発する際には