Search code examples
androidsqliteandroid-activityandroid-sqliteandroid-lifecycle

SQLiteDatabase error on recreate Activity


After intent is recreated i get exception on every database query. I think it may be because of way of recreating activity, but don't know how to solve this.

I'm recreating it after change of language and dark mode change.

Exception is thrown when recreate activity:

Attempt to re-open an already-closed object: SQLiteDatabase:

Activity

private lateinit var db: SQLiteDatabase
private lateinit var oh: LOTDatabaseHelper
...

onCreate

...
oh = LOTDatabaseHelper(this)
db = oh.readableDatabase
...

onDestroy

...
db.close()
oh.close()
...

and how I recreate intent

val intent = intent
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
startActivity(intent)
finish()

edit:

What I've found is that Activity is created about twice, then one is left and the rest is destroyed. OnDestroy seems to work for all Activities and closes Database, which shouldn't be possible.

Now i'm navigating to Splash screen and then to Activity which uses Database and everything is working. So Why lifecycle is not working well when Activity is replaced with herself?


Solution

  • The open an already closed database is commonly caused by mismatched opens and closes.

    Often the fix is to simply NOT open and close the database (unless specifically needed such as restoring the database from a backup copy, or forcing a WAL checkpoint to backup the database) but to instead just open the database once.

    There is frequently no need to keep opening and closing the database as when the App finishes the database will be closed (you can always close it it the top most activity's onDestroy). Furthermore opening the database is a resource costly, so many opens is resource wasteful/inefficient.

    I would suggest removing all of closes. You may also wish to consider a singleton approach and perhaps even have all the DB functionality within the Helper.

    As an example perhaps consider:-

    The Helper DBHelper :-

    class LOTDatabaseHelper(context: Context): SQLiteOpenHelper(
        context,
        DATABASE_NAME,
        null,
        DATABASE_VERSION) {
    
        companion object {
            const val DATABASE_NAME = "my.db"
            const val DATABASE_VERSION = 1
    
            @Volatile
            private var instance: SQLiteDatabase? = null
    
            fun getInstance(context: Context): SQLiteDatabase {
                if (instance == null) {
                    instance = LOTDatabaseHelper(context).writableDatabase
                }
                return instance as SQLiteDatabase
            }
    
        }
    
        override fun onCreate(db: SQLiteDatabase?) {
            db?.execSQL(Table1.CREATE_SQL)
        }
    
        override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
            TODO("Not yet implemented")
        }
    
        override fun onDowngrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
            super.onDowngrade(db, oldVersion, newVersion)
        }
    
        fun insert(name: String): Long {
            val cv = ContentValues()
            cv.put(Table1.COL_NAME,name)
            return instance!!.insert(Table1.TABLE_NAME,null,cv)
        }
    
        fun getAllFromTable1() : Cursor {
            return instance!!.query(Table1.TABLE_NAME,null,null,null,null,null,"${Table1.COL_NAME} ASC")
        }
    
        @SuppressLint("Range")
        fun logAll() {
            val csr = getAllFromTable1()
            var row = 1
            val nameix = csr.getColumnIndex(COL_NAME)
            while (csr.moveToNext()) {
                Log.d("CURSORINFO","Row ${row++} ${COL_NAME} is ${csr.getString(csr.getColumnIndex(
                    COL_NAME))}")
                //val test = csr.getString(csr.getColumnIndex("${COL_NAME}"))
    
            }
            csr.close()
        }
    
        // Table stuff
        class Table1 {
            companion object {
                const val TABLE_NAME = "table1"
                const val COL_ID = BaseColumns._ID
                const val COL_NAME = "${TABLE_NAME}_name"
                const val CREATE_SQL = "CREATE TABLE IF NOT EXISTS $TABLE_NAME (${COL_ID} INTEGER PRIMARY KEY, $COL_NAME TEXT);"
            }
        }
    }
    

    An initial Activity for calling the second Activity MainActivity :-

    class MainActivity : AppCompatActivity() {
        val TAG = "MAINACTIVITYINFO"
    
        private lateinit var dbother: DBHelper
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            dbother = DBHelper.getInstance(this)!!
            dbother.insert("TESTOTHER") // Use the Insert in the DBHelper
    
            /* of course you can still get an SQLiteDatabase */
            /* There is very little use getting readable as it gets writable */
            /* so get writable is more accurate description */
            var sqliteDatabase = dbother.writableDatabase //<<<<<< can be used
            logCursorInfo(dbother.allAsCursor)
    
            // NOW Start the other Activity
            Log.d(TAG,"starting Activity2")
            intent = Intent(this,Activity2::class.java)
            startActivity(intent)
            Log.d(TAG,"after starting Activity2")
    
        }
    
        override fun onDestroy() {
            super.onDestroy()
            Log.d("ONDESTROY","On Destroy invoked.")
        }
    
        override fun onResume() {
            super.onResume()
            Log.d(TAG,"On Resume invoked")
            dbother = DBHelper.getInstance(this)!!
        }
    
        //
        @SuppressLint("Range")
        fun logCursorInfo(csr: Cursor) {
            val nameix = csr.getColumnIndex(LOTDatabaseHelper.Table1.COL_NAME)
            val idix = csr.getColumnIndex(LOTDatabaseHelper.Table1.COL_ID)
            while (csr.moveToNext()) {
                //Log.d(TAG,"${LOTDatabaseHelper.Table1.COL_ID} = ${csr.getString(idix)} ${LOTDatabaseHelper.Table1.COL_NAME} = ${csr.getString(nameix)}")
                Log.d(TAG,"${csr.getString(csr.getColumnIndex(LOTDatabaseHelper.Table1.COL_NAME))}")
            }
        }
        companion object {
            var counter: Int = 0
        }
    }
    
    • This uses the DBHelper but gets an Instance, it then adds a few rows, extracts them then starts the 2nd Activity.

    Activity2 the activity where you are having the issues :-

    class Activity2 : AppCompatActivity() {
        val TAG = "ACTIVITY2INFO"
        private lateinit var dbother: DBHelper
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_2)
            Log.d(TAG,"Activity 2 Started (onCreate). $this")
    
            dbother = DBHelper.getInstance(this)!! // Get the Database
    
            dbother.insert("TESTOTHER_ACTIVITY2") // Add a row
            var csr = dbother.allAsCursor // get All the rows
            DatabaseUtils.dumpCursor(csr) // dump the cursor
            csr.close() // close the cursor
    
            /* Start another activity (will loop so stop with counter)*/
            Log.d(TAG,"Restarting Activity. counter is ${counter++} $this")
            if (counter < 3) {
                val intent = intent
                intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
                startActivity(intent) //<<<<<<<<<< RUN 1 Not the Way as starts a new activity
    
                /* Perhaps the following???? */
                // this.intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
                //this.recreate() //<<<<<<<<<< RUN 2 perhaps the way but no intent
            }
            Log.d(TAG,"Finishing Activity 2 $this")
            finish()
        }
    
        companion object {
            var counter: Int = 0
        }
    
        override fun onResume() {
            super.onResume()
            Log.d(TAG,"OnResume Invoked. $this")
        }
    
        override fun onDestroy() {
            super.onDestroy()
            Log.d(TAG,"OnDestory (after super call - before db close and helper close. $this)")
            Log.d(TAG,"OnDestory (after all. $this)")
        }
    }
    
    • This gets an (the) DBHelper instance, inserts a row, extracts all the data and then starts another activity NOTING that this would loop endlessly so hence the counter.

    Hopefully the comments will be of assistance.

    As you can see I've included quite a bit of logging. However, as suggested the database is never closed.

    Running the App

    Here's the results of running the App i.e. the log (from a fresh install) :-

    2021-10-06 19:52:50.715  D/DBHELPERINFO: getInstance returning instance a.a.so69454766kotlinsqlite.DBHelper@59860ab
    2021-10-06 19:52:50.717  D/MAINACTIVITYINFO: TESTOTHER
    2021-10-06 19:52:50.717  D/MAINACTIVITYINFO: starting Activity2
    2021-10-06 19:52:50.724  D/MAINACTIVITYINFO: after starting Activity2
    2021-10-06 19:52:50.734  D/MAINACTIVITYINFO: On Resume invoked
    2021-10-06 19:52:50.734  D/DBHELPERINFO: getInstance returning instance a.a.so69454766kotlinsqlite.DBHelper@59860ab
    2021-10-06 19:52:50.848  D/ACTIVITY2INFO: Activity 2 Started (onCreate). a.a.so69454766kotlinsqlite.Activity2@3269902
    2021-10-06 19:52:50.848  D/DBHELPERINFO: getInstance returning instance a.a.so69454766kotlinsqlite.DBHelper@59860ab
    2021-10-06 19:52:50.850  I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@950aa67
    2021-10-06 19:52:50.851  I/System.out: 0 {
    2021-10-06 19:52:50.852  I/System.out:    _id=1
    2021-10-06 19:52:50.852  I/System.out:    table1_name=TESTOTHER
    2021-10-06 19:52:50.852  I/System.out: }
    2021-10-06 19:52:50.852  I/System.out: 1 {
    2021-10-06 19:52:50.852  I/System.out:    _id=2
    2021-10-06 19:52:50.852  I/System.out:    table1_name=TESTOTHER_ACTIVITY2
    2021-10-06 19:52:50.852  I/System.out: }
    2021-10-06 19:52:50.852  I/System.out: <<<<<
    2021-10-06 19:52:50.856  D/ACTIVITY2INFO: Restarting Activity. counter is 0 a.a.so69454766kotlinsqlite.Activity2@3269902
    2021-10-06 19:52:50.864  D/ACTIVITY2INFO: Finishing Activity 2 a.a.so69454766kotlinsqlite.Activity2@3269902
    2021-10-06 19:52:51.020  D/ACTIVITY2INFO: Activity 2 Started (onCreate). a.a.so69454766kotlinsqlite.Activity2@7db7a75
    2021-10-06 19:52:51.020  D/DBHELPERINFO: getInstance returning instance a.a.so69454766kotlinsqlite.DBHelper@59860ab
    2021-10-06 19:52:51.021  I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@7fc80ae
    2021-10-06 19:52:51.021  I/System.out: 0 {
    2021-10-06 19:52:51.021  I/System.out:    _id=1
    2021-10-06 19:52:51.021  I/System.out:    table1_name=TESTOTHER
    2021-10-06 19:52:51.021  I/System.out: }
    2021-10-06 19:52:51.021  I/System.out: 1 {
    2021-10-06 19:52:51.021  I/System.out:    _id=2
    2021-10-06 19:52:51.022  I/System.out:    table1_name=TESTOTHER_ACTIVITY2
    2021-10-06 19:52:51.022  I/System.out: }
    2021-10-06 19:52:51.022  I/System.out: 2 {
    2021-10-06 19:52:51.022  I/System.out:    _id=3
    2021-10-06 19:52:51.022  I/System.out:    table1_name=TESTOTHER_ACTIVITY2
    2021-10-06 19:52:51.022  I/System.out: }
    2021-10-06 19:52:51.022  I/System.out: <<<<<
    2021-10-06 19:52:51.023  D/ACTIVITY2INFO: Restarting Activity. counter is 1 a.a.so69454766kotlinsqlite.Activity2@7db7a75
    2021-10-06 19:52:51.030  D/ACTIVITY2INFO: Finishing Activity 2 a.a.so69454766kotlinsqlite.Activity2@7db7a75
    2021-10-06 19:52:51.081  D/ACTIVITY2INFO: Activity 2 Started (onCreate). a.a.so69454766kotlinsqlite.Activity2@3433874
    2021-10-06 19:52:51.081  D/DBHELPERINFO: getInstance returning instance a.a.so69454766kotlinsqlite.DBHelper@59860ab
    2021-10-06 19:52:51.082  I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@5c713d1
    2021-10-06 19:52:51.082  I/System.out: 0 {
    2021-10-06 19:52:51.082  I/System.out:    _id=1
    2021-10-06 19:52:51.082  I/System.out:    table1_name=TESTOTHER
    2021-10-06 19:52:51.082  I/System.out: }
    2021-10-06 19:52:51.082  I/System.out: 1 {
    2021-10-06 19:52:51.083  I/System.out:    _id=2
    2021-10-06 19:52:51.083  I/System.out:    table1_name=TESTOTHER_ACTIVITY2
    2021-10-06 19:52:51.083  I/System.out: }
    2021-10-06 19:52:51.083  I/System.out: 2 {
    2021-10-06 19:52:51.083  I/System.out:    _id=3
    2021-10-06 19:52:51.083  I/System.out:    table1_name=TESTOTHER_ACTIVITY2
    2021-10-06 19:52:51.083  I/System.out: }
    2021-10-06 19:52:51.083  I/System.out: 3 {
    2021-10-06 19:52:51.083  I/System.out:    _id=4
    2021-10-06 19:52:51.083  I/System.out:    table1_name=TESTOTHER_ACTIVITY2
    2021-10-06 19:52:51.083  I/System.out: }
    2021-10-06 19:52:51.083  I/System.out: <<<<<
    2021-10-06 19:52:51.084  D/ACTIVITY2INFO: Restarting Activity. counter is 2 a.a.so69454766kotlinsqlite.Activity2@3433874
    2021-10-06 19:52:51.084  D/ACTIVITY2INFO: Finishing Activity 2 a.a.so69454766kotlinsqlite.Activity2@3433874
    2021-10-06 19:52:51.097  D/MAINACTIVITYINFO: On Resume invoked
    2021-10-06 19:52:51.097  D/DBHELPERINFO: getInstance returning instance a.a.so69454766kotlinsqlite.DBHelper@59860ab
    

    One thing of note is the the DBHelper instance that is retrieved is always DBHelper@59860ab (i.e. the single instance).

    As you have noted what happens with the Activity is that another is is started. You may wish to try using the commented out:-

            /* Perhaps the following???? */
            // this.intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
            //this.recreate() //<<<<<<<<<< RUN 2 perhaps the way but no intent
    
    • I'm not sure whether this would achieve what you desire though.