男子大学生の考えること

日々思ったこと。プログラミング、音楽、アニメなど、、、

android AutoCompleteTextViewにSimpleCursorAdapterを使う

AutoCompleteTextViewを使うと、入力補完機能の付いたEditTextが使えます。(ちなみに、AutoCompleteTextViewはEditTextのサブクラス。EditTextはTextViewのサブクラス。何か違和感を感じますが。)
どう使うかというと、AutoCompleteTextView.setAdapter(Adapter adapter)といったように、AutoCompleteTextViewのインスタンスにArrayAdapterか何かをsetすればいいです。しかし、SimpleCursorAdapterを使うときはある処理が必要になってきます。
とりあえず普通に使ってみました。
ここで使うDatabaseは次のようなものとします。
f:id:gobopiyo:20141118233055p:plain

AutoCompleteTextView inputTextView = (AutoCompleteTextView) findViewById(R.id.autoInput);
		
		DatabaseOpenHelper helper = new DatabaseOpenHelper(this);
		SQLiteDatabase database = helper.getWritableDatabase();	
		Cursor cursor = database.query("person", new String[]{"rowid as _id", "name"}, null, null, null, null, null);
		CursorAdapter adapter = new SimpleCursorAdapter(
				this, 
				android.R.layout.simple_spinner_item,
				cursor,
				new String[]{"name"},
				new int[]{android.R.id.text1},
				0);
		inputTextView.setAdapter(adapter);

実行すると、、、、
f:id:gobopiyo:20141118233256p:plain
できたと思って候補をタッチすると、
f:id:gobopiyo:20141118233349p:plain
ええ、、、
ということで、次の処理をsetAdapter()の前にします。

adapter.setCursorToStringConverter(new CursorToStringConverter() {
			@Override
			public String convertToString(Cursor cursor) {
				return cursor.getString(cursor.getColumnIndex("name"));
			}
		});

おそらく、候補をタッチしたとき、内部ではタッチしたカーソルのtoString()を入力しているためこのようになってしまうようです。なので、タッチしたCursorからどのようにStringの値を得るのかをこちらで指定する必要があるということです。
今回はカラムnameの値のみを候補に表示しましたが、adapterをいじれば複数の値を表示できます。

これでうまくいきました。
f:id:gobopiyo:20141118234317p:plain

自前のSQLiteDatabaseファイルをassetsからandroidで使えるようにする

androidではSQLiteが使えます。いろんな本なんかを読んでいると、androidの中で
テーブル定義して、データ挿入、削除、更新ができます。。。
みたいなことは書いてあるんですが、
あらかじめ自分で作ったDBファイルを使うことを書いた本がなかなかありません。
ググるといろいろな方法がありますが、
基本的にassetsフォルダに入れて、databaseフォルダにコピーする
ということです。無理やり感がありますがこれしかないみたい。
androidのdatabaseは
data/data/アプリのパッケージ/databases
にあります。
eclipseではファイルエクスプローラで見れますし、ファイルを保存することもできます。
ということで、ググった情報をもとにつくりました。

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

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

public class DatabaseOpenHelper extends SQLiteOpenHelper {
	
	private static final long serialVersionUID = 1L;
	private static final String DB_FILE_NAME = "database.sqlite"; //assetsにあるDBファイル名
	private static final String DB_NAME = "database.db"; //androidが使うDBファイル名
	private static final int DB_VERSION = 1;
	private Context context;
	private File dbPath;
	private boolean databaseExist = true; //適切なDBファイルが存在するか
	
	public DatabaseOpenHelper(Context context) {
		super(context, DB_NAME, null, DB_VERSION);
		this.context = context;
		this.dbPath = context.getDatabasePath(DB_NAME);
	}

	@Override
	public void onCreate(SQLiteDatabase db) {
		super.onOpen(db);
		databaseExist = false;
	}

	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		String databasePath = this.dbPath.getAbsolutePath();
		File file = new File(databasePath);
		if(file.exists()){
			file.delete();
		}
		databaseExist = false;
	}
	
	@Override
	public SQLiteDatabase getWritableDatabase() {
		SQLiteDatabase database = super.getWritableDatabase();
		if(!databaseExist){
			try {
				database.close();
				database = copyDatabaseFromAssets();
				databaseExist = true;
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return database;
	}
	
	
	/**
	 * assetsにあるデータベースをdata/data/package/databasesにコピーする
	 * @throws IOException
	 */
	private SQLiteDatabase copyDatabaseFromAssets() throws IOException{
		InputStream inputStream = this.context.getAssets().open(DB_FILE_NAME);
		OutputStream outputStream = new FileOutputStream(dbPath);
		
		byte[] buffer = new byte[1024];
		int size;
		while ((size = inputStream.read(buffer)) > 0) {
			outputStream.write(buffer, 0, size);
		}
		outputStream.flush();
		outputStream.close();
		inputStream.close();
		
		return super.getWritableDatabase();
	}

}

といった感じです。つかう側は

DatabaseOpenHelper helper = new DatabaseOpenHelper(context);
SQliteDatabase database = helper.getWritableDatabase();

と書くだけで、DBフォルダになかったらコピーしてから、あるいはバージョンが異なれば削除した後コピーしてから、適切なDBファイルがあればそのまま取得できます。
assetsのDBファイルを更新した際はDatabaseOpenHelperのDB_VERSIONを+すればいいです。

android cursoradapterで_id がないと怒られる

androidのSqliteDatabaseとSimpleCursorAdapterなんかを使っていると

つまずく原因頻出の

java.lang.IllegalArgumentException: column '_id' does not exist

_idっていうカラムがないぞ!!

とおこられる。

ググると確かに必要らしい。そうか、それだったら作ってあげるよと元のDBに_idを追加した。

 Cursor cursor = database.query("tableName", null, null, null, null, null, null);

これは

select * from tableName;

ということですね。

このあと

adapter = new SimpleCursorAdapter(this, android.R.layout.simple_spinner_item, cursor, 
new String[]{"columnName"}, new int[]{android.R.id.text1},0);

みたいな感じで使いますが、また_idがないと怒られます。
エミュレータのファイルには確かにあるはずなんだけどなー

なぜかはわかりませんがCursorの引数で_idを含んでいることが必要みたい。

(DBに_idを含んでいても)

そのあといろいろ試しましたが次で成功しました。

Cursor cursor = database.query("tableName", new String[]{"rowid as _id", "columnName"}, null, null, null, null, null);

select * としたいとき、SQLiteDatabase.queryの第二引数をnullにすればいいのですが、CursorAdapterで使うときはカラム名を指定する。さらに、SQLiteDatabaseはユーザが作っていなくてもデフォルトでrowidというオートナンバー型のカラムを常に持っています。なので、それを_idとしてしまおうということです。このようにすることで、わざわざDBに_idというカラムを持たせなくて済みます。なんか腑に落ちませんがこれでいきます。。