tags:

views:

333

answers:

2

I have an application with a ListActivity that uses a CursorAdapter as its adapter. The ListActivity opens the database and does the querying for the CursorAdapter, which is all well and good, but I am having issues with figuring out when to close both the Cursor and the SQLiteDatabase.

The way things are handled right now, if the user finishes the activity, I close the database and the cursor. However, this still ends up with the DalvikVM warning me that I've left a database open - for example, if the user hits the "home" button (leaving the activity in the task's stack), rather than the "back" button.

If I close them during pause and then re-query during resume, then I don't get any errors, but then a user cannot return to the list without it requerying (and thus losing the user's place in the list). By this I mean, the user can click on any item in the list and open a new activity based on it, but will often want to hit "back" afterwards and return to the same place on the list. If I requery, then I cannot return the user back to the correct spot.

What is the proper way to handle this issue? I want the list to remain scrolled properly, but I don't want the VM to keep complaining about unclosed databases.

Edit: Here's a general outline of how I handle the code at the moment:

public class MyListActivity extends ListActivity {
    private Cursor mCursor;
    private CursorAdapter mAdapter;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAdapter = new MyCursorAdapter(this);
        setListAdapter(mAdapter);
    }

    protected void onPause() {
        super.onPause();
        if (isFinishing()) {
            mCursor.close();
        }
    }

    protected void onDestroy() {
        super.onDestroy();
        mCursor.close();
    }

    private void updateQuery() {
        // If we had a cursor open before, close it.
        if (mCursor != null) {
            mCursor.close();
        }
        MyDbHelper dbHelper = new MyDbHelper(this);
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        mCursor = db.query(...);
        mAdapter.changeCursor(mCursor);
        db.close();
    }
}

updateQuery() can be called multiple times because the user can filter the results via menu items (I left this part out of the code, as the problem still occurs even if the user does no filtering).

Again, the issue is that when I hit home I get leak errors. Yet, after going home, I can go back to the app and find my list again - cursor fully intact.

A: 

Close the Cursor and SQLiteDatabase in onDestroy(), for those things not already closed, and see if that helps. There may be scenarios where isFinishing() is false in onPause(), yet onDestroy() still winds up being called.

CommonsWare
This doesn't solve the main issue I have, which is that when a user hits the "home" button, the db isn't closed. onDestroy() isn't called when the user hits "home", but if the user does hit "home" and goes to another application then the Activity is killed without another line of code being executed. I can't call close() during onPause() or onStop(), because that's too early.Perhaps this is just a limitation of Android, but I don't like seeing those unclosed databases. Perhaps a DB pool would fix this problem?
Daniel Lew
You will get the "leak found" exception if your SQLiteDatabase object is garbage collected and not closed. There must be something you are doing, therefore, to let go of that SQLiteDatabase object while your activity is stopped.
CommonsWare
I've added some code to show what I'm doing. I'm really not sure how I can better stop the leak, maybe this will help.
Daniel Lew
I am surprised your code works. I thought you had to keep the database open while the cursor was open. When exactly do you get the leak message: immediately on pressing HOME, after some time sitting on the home screen after pressing HOME, on re-launching the application from the launcher, or something else?
CommonsWare
Initially, I kept the db open while the cursor was open; but then I found that the code worked even when I closed the db. Anyways, you get the error after pressing HOME and then sitting on it for a bit.
Daniel Lew
Well, all I can tell you is that I cannot reproduce the problem, albeit using a managed cursor and closing the database in onDestroy(). I would recommend you consider adopting a new tack and figuring out if there is a way to get what you deem to be an acceptable UI while still using managed cursors.
CommonsWare
Managed cursors fix the issue, but then I can't open a new activity (via a selected item on the list) and return later, because the managed cursor is destroyed when opening a new activity. Basically, I can either be uber-careful (but lose the user's place in the list constantly) and never get this error, or I can get the error occasionally but allow the user to return to the same place on a list after opening a sub-activity.
Daniel Lew
Or, you can find a way to "allow the user to return to the same place on a list after opening a sub-activity" *despite* using a managed cursor. Your current approach has been to blame the managed cursor. I'd blame the list. I thought that there was an API to ensure that a certain list entry was visible, but I couldn't find it when I poked around. But, it might be worthwhile for you to ask that question (separately) and see if you get better answers.
CommonsWare
A: 

I usually have one SQLiteOpenHelper per app:

public class MyApp extends Application {

 private static MyDbHelper dbHelper;

 @Override
 public void onCreate() {
  super.onCreate();
  dbHelper = new MyDbHelper(this);
 }

 @Override
 public void onTerminate() {
  super.onTerminate();
  dbHelper.close();
 }

 public static SQLiteDatabase getDB() {
  return dbHelper.getWritableDatabase();
 }

}

Haven't had any problems with this approach so far.
And you can use startManagingCursor(..) available in Activity subclasses (though I let Adapters manage Cursors):

public class SomeAdapter extends CursorAdapter {

    public void setNewFilter(CharSequense query){
        // in fact, I use a DAO here
        Cursor c = MyApp.getDB().query(..);
        changeCursor(c);
    }

}
alex