androidでprivateメソッドのテストをする

そういえば書いてなかったのでメモ。

    /**
     * Hogeクラスのprivateメソッドadd(int a, int b)をテストする
     * @throws SecurityException
     * @throws IllegalArgumentException
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    public void testAdd() throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        Hoge hoge = new Hoge();
        Class<Hoge> clz = Hoge.class;
        Method method = clz.getDeclaredMethod("add", Integer.class, Integer.class);
        method.setAccessible(true);
        int result = (int) method.invoke(hoge, 5, 3);
        asserTrue(result == 8);
    }

■staticメソッド

    /**
     * Hogeクラスのprivate staticメソッドadd(int a, int b)をテストする
     * @throws SecurityException
     * @throws IllegalArgumentException
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    public void testAdd() throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        Class<Hoge> clz = Hoge.class;
        Method method = clz.getDeclaredMethod("add", Integer.class, Integer.class);
        method.setAccessible(true);
        int result = (int) method.invoke(clz, 5, 3);
        asserTrue(result == 8);
    }

■プライベートフィールド

Hoge hoge = new Hoge();
Field f = hoge.getClass().getDeclaredField("f");
f.setAccessible(true);
assertEquals(value, f.get(hoge));
//assertEquals(Piyo.class, f.get(hoge).getClass());

androidでArrayAdapterを使う

■実装

mListView = (ListView) findViewById(R.id.list);
mListView.setAdapter(mOriginalArrayAdapter);

mOriginalArrayAdapterを生成するとする。

public class OriginalArrayAdapter extends ArrayAdapter<OriginalDataRow> {
    private LayoutInflater inflater;

    /**
     * セル内における可変要素
     */
    public static class ViewHolder {
        public TextView text;
    }

    public OriginalArrayAdapter(Context context, int textViewResourceId, ArrayList<OriginalDataRow> data) {
        super(context, textViewResourceId, data);
        inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final OriginalDataRow data = getItem(position);// フィールドなどから取得してはいけない...(a)
        
        // viewのセットアップ
        ViewHolder holder = null;
        if (convertView == null){
            holder = new ViewHolder();
            convertView = inflater.inflate(R.layout.cell_original_data, null);
            holder.text = (TextView) convertView.findViewById(R.id.text);
            convertView.setTag(holder);
        }
        else {
            holder = (ViewHolder) convertView.getTag();
        }
        
        // dataをviewに書き出す
        holder.text.setText(data.text);
        return convertView;
    }
}

filterを使用する

    @Override
    public void onTextChanged(CharSequence s, int start, int count, int after) {
        String query = mEditText.getText().toString();
        if (query.equals("")) {
            mListView.clearTextFilter();
        }
        else {
            mListView.setFilterText(query);
        }
    }

(a)の部分でsuperクラスのgetItemを使用しないとフィルタリングが上手く適用されない。また、filterを使用する場合は、リストアイテムのtoString値が使用されるため、以下のように必要に応じてOverrideする必要がある。

public class OriginalDataRow {
    public String name;
    @Override
    public String toString() {
        return name;// filterに使用する文字列
    }
}

androidのSQLiteでAUTOINCREMENT値を取得する

SELECT `seq` FROM sqlite_sequence WHERE `name` = 'TABLENAME';

上述を踏まえて以下のようにすることで取得できる。

    /**
     * auto_incrementで代入した最大値
     * @return
     */
    public long getLastInsertId() {
        long index = 0;
        SQLiteDatabase sdb = getReadableDatabase();
        Cursor cursor = sdb.query(
                "sqlite_sequence",
                new String[]{"seq"},
                "name = ?",
                new String[]{TABLENAME},
                null,
                null,
                null,
                null
        );
        if (cursor.moveToFirst()) {
            index = cursor.getLong(cursor.getColumnIndex("seq"));
        }
        cursor.close();
        return index;
    }

robotiumを使ってみる

@ayunyanさんに教えていただいたので使ってみる。

■シナリオ

以下のようにandroid版顔文字辞典を操作するとする。

  1. メニューを開く
  2. カテゴリ追加をタップする
  3. カテゴリ名を入力し登録する(a)
  4. 登録したカテゴリ(a)をロングタップする
  5. 編集を選択する
  6. カテゴリ名を変更して登録する(b)
  7. 編集したカテゴリ(b)をロングタップする
  8. 削除をタップする

■テストコード

以下のようにして「みんなの顔文字辞典」のカテゴリー追加・編集・削除のダイアログのテストを行う。

package info.justoneplanet.android.kaomoji.test.dialog;

import com.jayway.android.robotium.solo.Solo;

import info.justoneplanet.android.kaomoji.KaomojiFavorite;
import info.justoneplanet.android.kaomoji.R;
//import android.app.Instrumentation;
import android.test.ActivityInstrumentationTestCase2;

/**
 * カテゴリを追加するダイアログのテストケース
 * @author justoneplanet
 * カテゴリの登録・編集・削除は決まった順番で行われる必要があるためメソッド名で順序を指定した
 */
public class CategoryDialogBuilderTest extends ActivityInstrumentationTestCase2<KaomojiFavorite> {

    private KaomojiFavorite mActivity;
    //private Instrumentation mInstrumentation;
    private Solo solo;
    
    private static final String OLD_CATEGORY_NAME = "新しいカテゴリ";
    private static final String NEW_CATEGORY_NAME = "新カテゴリ";

    public CategoryDialogBuilderTest() {
        super("info.justoneplanet.android.kaomojifavorite", KaomojiFavorite.class);
    }
    
    @Override
    public void setUp() throws Exception {
        super.setUp();
        mActivity = getActivity();
        //mInstrumentation = getInstrumentation();
        solo = new Solo(getInstrumentation(), getActivity());
    }
    
    @Override
    public void tearDown() throws Exception {
        super.tearDown();
        mActivity = null;
        //mInstrumentation = null;
    }
    
    /**
     * カテゴリを正しく追加できるか
     * カテゴリaを登録する
     */
    public void testCategory1Add() {
        solo.clickOnMenuItem(mActivity.getString(R.string.menu_category_add));// メニュー>カテゴリを追加
        solo.enterText(0, OLD_CATEGORY_NAME);// 新しいカテゴリ名を入力
        solo.clickOnButton(mActivity.getString(R.string.register));// 登録ボタン
    }
    
    /**
     * カテゴリを正しく編集できるか
     * カテゴリaを編集してカテゴリbに名前を変える
     */
    public void testCategory2Edit() {
        solo.clickLongOnText(OLD_CATEGORY_NAME);// テキストで探索してリストの要素をロングタップ
        solo.clickOnText(mActivity.getString(R.string.edit));// ダイアログ内カテゴリの編集をタップ
        solo.clearEditText(0);// テキストを消す
        solo.enterText(0, NEW_CATEGORY_NAME);// 新カテゴリ名を入力
        solo.clickOnButton(mActivity.getString(R.string.register));// 登録ボタン
    }
    
    /**
     * カテゴリを削除できるか
     * カテゴリbを削除する
     */
    public void testCategory3Delete() {
        solo.clickLongOnText(NEW_CATEGORY_NAME);// テキストで探索してリストの要素をロングタップ
        solo.clickOnText(mActivity.getString(R.string.delete));// カテゴリの削除をクリック
        solo.waitForDialogToClose(1000);// すぐに終了してしまうと削除クエリが実行されなかったので
    }
}

操作する要素をテキストで指定できる探索が便利!

clickInListについて

以下のようにして使用した。おそらく表示しているlistアイテムの配列の中でのpositionを指定していると思われる。また、表示されていないlistアイテムを上からの位置で指定することはできなかった。

private String tapEmoticonAfterCategory(String category, int position) {
    solo.clickOnText(category);
    solo.waitForDialogToClose(1000);// 作用するまでwait
    ArrayList<TextView> list = solo.clickInList(position);// 表示されているlistの要素配列のposition
    solo.waitForDialogToClose(1000);// 作用するまでwait
    Log.e("tapped", String.valueOf(list.get(0).getText()));
    return String.valueOf(list.get(0).getText());
}

■参考

おまけ

当初は以下のようにThread.sleepを使用していたが、ProgressDialogを表示し通信結果を待つような場合に期待した動作にならなかった。

    /**
     * カテゴリを削除できるかテストする
     * @throws InterruptedException 
     */
    public void testCategory3Delete() throws InterruptedException {
        solo.clickLongOnText(NEW_CATEGORY_NAME);// テキストで探索してリストの要素をロングタップ
        solo.clickOnText(mActivity.getString(R.string.delete));// カテゴリの削除をクリック
        Thread.sleep(1000);// 期待の動作とならない
    }

テキスト(半角カナ?)によってはいまいち探索できないときがある。

AsyncTaskのテストをする

■コード

以下のようにUIスレッドでAsyncTaskをインスタンス化する必要がある。

public class EveryoneTaskTest extends ActivityInstrumentationTestCase2<Kaomoji> {
    private Kaomoji mActivity;
    private Instrumentation mInstrumentation;
    private EveryoneTask everyoneHttp;
    private CountDownLatch countDownLatch;
    private String mResult;
    
    public EveryoneTaskTest() {
        super("info.justoneplanet.android.kaomoji", Kaomoji.class);
    }
    
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mActivity = getActivity();
        mInstrumentation = getInstrumentation();
        countDownLatch = new CountDownLatch(1);
    }
    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
        mActivity = null;
        mInstrumentation = null;
    }
    
    public void testExecute() throws InterruptedException, JSONException {
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                everyoneHttp = new EveryoneTask(new EveryoneTask.Observer(){
                    @Override
                    public void onHttpUpdate(String result, MODE mode) {
                        mResult = result;
                        countDownLatch.countDown();// カウントを0にする
                    }
                });
                everyoneHttp.execute(null);
            }
        });
        countDownLatch.await();// countが0になるまで処理を待機
        
        // 要素の検証
        JSONObject elm;
        JSONArray ary = new JSONArray(mResult);
        assertTrue(ary.length() == 150);
    }
}

失敗例

以下のようにアプリのメインスレッド以外でAsyncTaskのインスタンス化を行なってはならない。挙動が不安定になりonPostExecuteが実行されない。

/**
 * みんなな顔文字を通信して取得するクラスのテストケース
 * @author justoneplanet
 */
public class EveryoneHttpTest extends AndroidTestCase implements EveryoneHttp.Observer {
    CountDownLatch countDownLatch;
    private EveryoneHttp everyoneHttp;
    private String mResult;
    
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        countDownLatch = new CountDownLatch(1);
    }

    public void testDoInBackground() throws InterruptedException, JSONException {
        everyoneHttp = new EveryoneHttp(this);// get everyone's emoticons from transfer
        everyoneHttp.execute();
        countDownLatch.await();// countが0になるまで処理を待機
        
        JSONArray ary = new JSONArray(mResult);
        assertTrue(ary.length() == 150);// 要素数の検証
        
        // 要素の検証
        JSONObject elm = ary.getJSONObject(0);
        Log.e("elm", elm.toString());
        assertFalse(elm.getString("face").equals(null));
        assertFalse(elm.getString("tag").equals(null));
    }
    
    /**
     * 通信完了後に実行される
     */
    @Override
    public void onHttpUpdate(String result) {
        mResult = result;
        countDownLatch.countDown();// カウントを0にする
    }
}

非同期処理自体はAsyncTaskをwrapしたクラスで行なっている。Observerを使ったのでテストコードもすっきり。

androidでAndroidWebDriverを使ってみる

元ネタはTim BrayのIntroducing Android WebDriverという記事。

■準備

テストプロジェクトで右クリックし「propaties> Java Build Path > Libraries > Add External JARs」で以下のjarをインポートする。

  • android_webdriver_library-srcs.jar
  • android_webdriver_library.jar
  • guava-10.0.1.jar

■コード

Tim様の記事ではgoogle.comが使用されているが以下のような理由によってテストにパスしない。

  • ロケールが違うので日本語表示される
  • 結果がリッチすぎて上手くdomを抽出できない

テストコード内でgoogle.comのリンクをクリックするようにできると思うが、html構造を把握するのが大変なので以下のようなコードでmozillaのサイトでテストすることにした。

package bootcamp2011.android.test;

import java.util.List;

import bootcamp2011.android.Bootcamp2011Activity;
import android.test.ActivityInstrumentationTestCase2;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.android.AndroidWebDriver;;

public class Bootcamp2011ActivityTest extends
        ActivityInstrumentationTestCase2<Bootcamp2011Activity> {

    private AndroidWebDriver driver;

    public Bootcamp2011ActivityTest() {
        super("bootcamp2011.android.bootcamp2011activity", Bootcamp2011Activity.class);
    }
    
    @Override
    protected void setUp() throws Exception {
      driver = new AndroidWebDriver(getActivity());
    }
    
    @Override
    protected void tearDown() {
       driver.quit();
    }
    
    public void testGoogleShouldWork() {
        // mozillaのサイトを訪問
        driver.get("https://developer.mozilla.org/ja/JavaScript");

        // 検索boxを抽出して文字入力し送信
        WebElement searchBox = driver.findElement(By.id("q"));
        searchBox.sendKeys("querySelector");
        searchBox.submit();

        // 検索結果にquerySelectorという文字列が含まれているかテスト(仮)
        WebElement resultSection = driver.findElement(By.id("search-results"));
        List<WebElement> searchResults = resultSection.findElements(By.tagName("table"));
        WebElement result = searchResults.get(0);
        assertTrue(result.getText().contains("querySelector"));
    }
}

By.cssSelector()というメソッドがあるのだが上手く働かない…一応、動作するサンプルが「android-sdk-mac_x86/extras/google/webdriver/TestAnAndroidWebApp/src/simple/app/test/SimpleGoogleTest.java」にある。

SimpleCursorTreeAdapterのgetChildrenCursorが生成時にも実行される

android 2.3未満で起こった挙動のメモ。android 2.3以降では起こらない。

メモ

以下のようにして開閉可能なListActivityを定義する。

public class KaomojiFavorite extends ExpandableListActivity implements FavoriteHelper.Listener {
    private FavoriteHelper favoriteHelper;
    private CategoryHelper categoryHelper;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getExpandableListView().setCacheColorHint(0);// scroll時の背景
        
        // カテゴリを取得
        categoryHelper = new CategoryHelper(getApplicationContext(), null);
        Cursor categoryCursor = categoryHelper.getCursor();
        startManagingCursor(categoryCursor);
        
        // お気に入りを取得してlistにセット
        favoriteHelper = new FavoriteHelper(getApplicationContext(), null);
        ExpandableListAdapter adapter = favoriteHelper.getSimpleAdapter(this, categoryCursor);
        setListAdapter(adapter);

    }

カテゴリ毎にお気に入りを表示する仕組みだ。FavoriteHelperクラスの実装は以下のようになっている。

    public ExpandableListAdapter getSimpleAdapter(Listener listener, Cursor categoryCursor)
    {
        mListener = listener;
        ExpandableListAdapter adapter = new ExpandableListAdapter(
                mContext,
                categoryCursor,
                R.layout.category,
                new String []{"_id", "name"},
                new int []{R.id.list_id, R.id.list_category},
                R.layout.list,
                new String []{"item", "_id"},
                new int []{R.id.list_item, R.id.list_id}
        );
        return adapter;
    }
    
    /**
     * categoryデータ(Cursor)を元にしてExpandableListActivity用のAdapterを生成する
     * @author justoneplanet
     */
    public class ExpandableListAdapter extends SimpleCursorTreeAdapter {
        
        public ExpandableListAdapter(Context context, Cursor groupCursor,
                int groupLayout, String[] groupFrom, int[] groupTo,
                int childLayout, String[] childFrom, int[] childTo) {
            super(context, groupCursor, groupLayout, groupFrom, groupTo, childLayout, childFrom, childTo);
        }
        
        /**
         * カテゴリ用のCursorからIDを取得して子データを検索取得する
         * android 2.2以前ではアプリ起動時にも実行される
         */
        @Override
        protected Cursor getChildrenCursor(Cursor groupCursor) {
            final long idCategory = groupCursor.getLong(groupCursor.getColumnIndex("_id"));
            SQLiteDatabase sdb = getReadableDatabase();
            Cursor cursor = sdb.query(
                    "favorite",
                    new String[]{"_id", "id_category", "item"},
                    "id_category = ?",
                    new String[]{String.valueOf(idCategory)},
                    null,
                    null,
                    null,
                    null
            );
            mListener.onGetChildrenCursor(cursor);
            return cursor;
        }
    }

以上。めも。

androidでTouchUtilsを使ってテストをする

以下のようにしてTouchUtilsを使用することができる。

public void testPushListByTouchUtils()
{
    LinearLayout item = (LinearLayout) mActivity.getListView().getChildAt(0);
    //TouchUtils.tapView(this, item);// 画面に触れている状態
    TouchUtils.clickView(this, item);// 画面から指が離れた状態
    String face = (String) ((TextView) item.findViewById(R.id.list_face)).getText();
    ClipboardManager cm = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
    assertEquals(face, cm.getText());
}

SecurityExceptionなどが発生すると書いてある文献も見られるが自分の環境では発生しなかった。但し、Viewの選択が間違っていてnullであったりすると発生する(なぜNullPointer〜でないのか…)。ちなみに同様のテストケースをTouchUtilsを使用せずに書くと以下のようになる。

public void testPushList()
{
    mActivity.runOnUiThread(new Runnable() {
        @Override
        public void run() {
            LinearLayout item = (LinearLayout) mActivity.getListView().getChildAt(0);
            mActivity.getListView().performItemClick(item, position, 0));
            String face = (String) ((TextView) item.findViewById(R.id.list_face)).getText();
            ClipboardManager cm = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
            MainActivityTest.assertEquals(face, cm.getText());
        }
    });
    mInstrumentation.waitForIdleSync();
}

TouchUtilsは別アプリがタップするようなイメージで、Viewに対して発生させるイメージだ。従って、performItemClickはUIThreadで実行する必要がある。

AsyncQueryHandlerを使ってSQLiteのクエリを非同期処理する

特に重いクエリは使ってないんだが、とりあえずAsyncQueryHandlerを使ってSQLiteのクエリを非同期で処理する。

■実装

以下のようにmanifestファイルのapplication要素内にproviderを追加する。

<provider 
    android:name=".Provider"
    android:authorities="info.justoneplanet.android.sample.provider"<!--アクセスするためのURI-->
    android:exported="false"/><!--非公開にしないと他のアプリからアクセスできてしまう-->

セキュリティには注意する。

Helper.java

class Helper extends SQLiteOpenHelper {
    private static final int VERSION = 1;
    private static final String FILENAME   = "info.justoneplanet.android.sample.db";
    static final String TABLENAME   = "tbl";
    static final String ID          = "_id";
    static final String CREATED     = "created";
    static final String NAME        = "name";
    static Helper INSTANCE = null;
    
    private Helper(Context context, CursorFactory factory) {
        super(context, FILENAME, factory, VERSION);
    }
    public static Helper getInstance(Context context, CursorFactory factory) {
        if (INSTANCE == null) {
            INSTANCE = new Helper(context, factory);
        }
        return INSTANCE;
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(
            "CREATE TABLE `" + TABLENAME + "`(" +
            " `" + ID         + "` INTEGER PRIMARY KEY AUTOINCREMENT," +
            " `" + CREATED    + "` INTEGER," +
            " `" + NAME       + "` TEXT" +
            ");"
        );
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

Provider.java

以下のようにContentProviderクラスを継承したProviderクラスを作る。

public class Provider extends ContentProvider {
    @Override
    public String getType(Uri uri) {
        return null;
    }
    @Override
    public boolean onCreate() {
        return false;
    }
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getReadableDatabase();
        Cursor cursor = sdb.query(
                Helper.TABLENAME,
                new String[]{Helper.ID, Helper.NAME, Helper.CREATED},
                selection,
                selectionArgs,
                null,
                null,
                sortOrder,
                null
        );
        return cursor;
    }
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        sdb.insert(Helper.TABLENAME, null, values);
        getContext().getContentResolver().notifyChange(uri, null);
        return null;
    }
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        int rows = sdb.update(Helper.TABLENAME, values, selection, selectionArgs);
        getContext().getContentResolver().notifyChange(uri, null);
        return rows;
    }
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        int rows = sdb.delete(Helper.TABLENAME, selection, selectionArgs);
        getContext().getContentResolver().notifyChange(uri, null);
        return rows;
    }
}

Table.java

public class Table {
    private static final Uri URI = Uri.parse("content://info.justoneplanet.android.sample.provider/");
    private Context mContext;
    
    public Table(Context context) {
        mContext = context;
    }
    
    /**
     * データを取得する
     * @return ListActivity用のadapter
     */
    public void load(final LoadObserver observer)
    {
        AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) {
            @Override
            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
                super.onQueryComplete(token, cookie, cursor);
                observer.onLoadCursor(cursor);
            }
        };
        handler.startQuery(0, null, URI, null, null, null, Helper.CREATED + " DESC");
    }
    /**
     * 保存する
     * @return
     */
    public void add(String name) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(Helper.NAME, name);
        contentValues.put(Helper.CREATED, new Date().getTime());
        AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) {
            @Override
            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
                super.onQueryComplete(token, cookie, cursor);
            }
        };
        handler.startInsert(0, null, URI, contentValues);
    }
    /**
     * 一定時間経過したものを削除する
     * @param created
     * @return
     */
    public void delete(long created) {
        AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) {
        };
        handler.startDelete(0, null, URI, Helper.CREATED + " < ?", new String[]{String.valueOf(created)});
    }
    /**
     * helper経由でdbをcloseする
     */
    public void close() {
        Helper.getInstance(mContext, null).close();
    }
    public interface LoadObserver {
        public void onLoadCursor(Cursor cursor);
    }
}

CursorからAdapterを生成する

■実装

以下のようにすることでListActivityで使うadapterをcursorから直接生成できる。

SQLiteDatabase sdb = getReadableDatabase();
Cursor cursor = sdb.query(
        "table",
        new String[]{"_id", "name", "address", "created"},
        null,
        new String[]{},
        null,
        null,
        "created DESC",
        null
);

// Cursorを元にしてListActivity用のadapterを生成する
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
        mContext,
        R.layout.list,
        cursor,// datasource
        new String[]{"name", "address"},// データベースカラム名...(a)
        new int[]{R.id.list_face, R.id.list_tag}// (a)に対応するTextViewのid
);

上述の方法ではSQLiteで取得したデータになんらかの文字列を加えたりすることはできない。

■表示データの加工

以下のようにSimpleCursorAdapterクラスを継承することでデータを加工して表示できる。

static class ArrangeListAdapter extends SimpleCursorAdapter {
    public ArrangeListAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
        super(context, layout, c, from, to);
        setViewBinder(new ArrangeListViewBinder());
    }
    
    static class ArrangeListViewBinder implements SimpleCursorAdapter.ViewBinder {
        @Override
        public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
            if (columnIndex == cursor.getColumnIndex("name")) {// nameの時に文字列を付加する
                String p = cursor.getString(columnIndex);// viewにはまだattachされていないのでcursorから取得
                ((TextView) view).setText(p + " さん");
                return true;
            }
            return false;
        }
    }
}