1
votes

I have a SQLite query returning thousands of rows, which I want to visualize using a ListView.

In order to keep my UI thread responsive, I create the ListAdapter on a background thread.

However the statement that takes most time (and can cause ANR) is ListActivity.setListAdapter which I have to execute on the UI thread... Any advice?

public class CursorTestActivity extends ListActivity {

    private static final String LOGTAG = "DBTEST";

    private DatabaseManager mDbManager;
    private Cursor mCursor;
    private HandlerThread mIOWorkerThread;
    private Handler mIOHandler;
    private Handler mUIHandler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mDbManager = new DatabaseManager(this);

        mUIHandler = new Handler();
        createIOWorkerThread();

        log("creating cursor");
        mCursor = mDbManager.getCursor(); // does db.query(...)
        startManagingCursor(mCursor);

        mIOHandler.post(new Runnable() {
            @Override
            public void run() {
                setMyListAdapter();
            }
        });
        log("onCreate done");
    }

    private void setMyListAdapter() {
        log("constructing adapter");
        // CustomCursorAdapter implements bindView and newView
        final CustomCursorAdapter listAdapter = new CustomCursorAdapter(this,
                mCursor, false);
        log("finished constructing adapter");
        mUIHandler.post(new Runnable() {
            @Override
            public void run() {
                log("setting list adapter");
                setListAdapter(listAdapter); // gets slower the more rows are returned
                log("setting content view");
                setContentView(R.layout.main);
                log("done setting content view");
            }
        });
    }

    private void createIOWorkerThread() {
        mIOWorkerThread = new HandlerThread("io_thread");
        mIOWorkerThread.start();
        Looper looper = mIOWorkerThread.getLooper();
        mIOHandler = new Handler(looper);
    }

    private void destroyIOWorkerThread() {
        if (mIOWorkerThread == null)
            return;
        Looper looper = mIOWorkerThread.getLooper();
        if (looper != null) {
            looper.quit();
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mDbManager != null)
            mDbManager.close();
        destroyIOWorkerThread();
    }

    private static void log(String s) {
        Log.d(LOGTAG, s);
    }

}
2

2 Answers

0
votes

Cursors are lazy loaded therefore at first data access the cursor is loaded - getCount is such access. The setAdapter method is invoking getCount on cursor - there is the performance issue.

  • You can set empty cursor in adapter and display empty list during loading cursor. Then use changeCursor method in adapter to change cursor to the new one.
  • You can fetch at first for example 100 rows in the first query, then load more rows in the background and changeCursor to the new one.
  • Or you can create own implementation of Cursor that has own implementation of getCount and fetches demanded rows on request.
0
votes
  1. consider using PagedListAdapter. it's something similar as proposed by @pawelziemba but provided by Android Framework https://developer.android.com/reference/android/arch/paging/PagedListAdapter -- if you can use Room framework you can get that for free
  2. load data async in ViewHolder - remember to cancel work if viewHolder gets rebinded and cache loaded data if needed