Search code examples
androidsqlitekotlinkotlin-lateinit

I am using sqlite incorrectly in this kotlin class


I started learning kotlin recently, and I am trying to implement a database using SQLite. When I try using the functions the app crashes. I don't know how to find the error log so I have added the functions I made and where the functions are implemented. Thank you for your help.

package com.example.mrtayyab.sqlitedbkotlin

import android.content.ClipDescription
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

class DatabaseHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, 1) {


    companion object {

        val DATABASE_NAME = "passwords.db"
        val TABLE_NAME = "passwords_table"
        val COL_1 = "ID"
        val COL_2 = "DESCRIPTION"
        val COL_3 = "PASSWORD"
        val COL_4 = "DATE_TIME"
    }


    override fun onCreate(db: SQLiteDatabase) {

        db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_NAME(ID INTEGER PRIMARY KEY AUTOINCREMENT , DESCRIPTION  TEXT , PASSWORD TEXT , DATE_TIME INTEGER)")

    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {

        db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME")
        onCreate(db)

    }

    fun insertData(description: String, password: String, date_time: String): Boolean? {
        val db = this.writableDatabase
        val cv = ContentValues()
        cv.put(COL_2, description)
        cv.put(COL_3, password)
        cv.put(COL_4, date_time)
        val res = db.insert(TABLE_NAME, null, cv)
        return !res.equals(-1)
    }

    fun getData(id: String, COL_NUM: String): Cursor {
        val column = arrayOf("$COL_NUM")
        val columnValue = arrayOf("$id")
        val db = this.writableDatabase
        return db.query("$TABLE_NAME", column, "$COL_1", columnValue, null, null, null )
    }

    fun getTableCount(): Long {
        val db = this.readableDatabase
        val count = DatabaseUtils.queryNumEntries(db, TABLE_NAME)

        return count
    }
}

Here is the code where it is implemented

package com.example.juliaojonah_hci_outcome2_v1

import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.mrtayyab.sqlitedbkotlin.DatabaseHelper
import com.google.gson.Gson
import kotlinx.android.synthetic.main.activity_main.*
import java.text.DateFormat
import java.util.*
import kotlin.collections.ArrayList

class passwords : AppCompatActivity() {

    lateinit var myDb: DatabaseHelper

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_passwords)



        val dateTime = Calendar.getInstance().time

        val dateFormatted = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(dateTime)

        val dateTextView : TextView = findViewById(R.id.textDate2) as TextView
        dateTextView.setText(dateFormatted)

        val recyclerView = findViewById(R.id.savedPasswords) as RecyclerView

        recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)

        val counter = myDb.getTableCount()

        val passwords = ArrayList<PasswordCluster>()

        if (counter > 0) {
            for (i in 1..counter) {
                var id = i
                var description = myDb.getData(id.toString(), "COL_2")
                var password = myDb.getData(id.toString(), "COL_3")
                var dateTime = myDb.getData(id.toString(), "COL_4")

                passwords.add(PasswordCluster(description.toString(), password.toString(), dateTime.toString()))
            }
        }

/*
        passwords.add(PasswordCluster("Amazon", "56,78,90,12", "Friday"))
        passwords.add(PasswordCluster("Amazon", "56,78,90,12", "Friday"))
        passwords.add(PasswordCluster("Amazon", "56,78,90,12", "Friday"))
        passwords.add(PasswordCluster("Amazon", "56,78,90,12", "Friday"))
        passwords.add(PasswordCluster("Amazon", "56,78,90,12", "Friday"))
*/
        val adapter = CustomAdapter(passwords)

        recyclerView.adapter = adapter
    }

    fun firstActivity(view: View){
        val intent = Intent(this, MainActivity::class.java)
        startActivity(intent)
    }
}

I use the save data function in this last piece of code and it doesn't crash, but I don't know if it is working correctly

package com.example.juliaojonah_hci_outcome2_v1

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.mrtayyab.sqlitedbkotlin.DatabaseHelper
import com.google.gson.Gson
import kotlinx.android.synthetic.main.activity_main.*
import java.security.spec.PSSParameterSpec
import java.text.DateFormat
import java.util.*
import kotlin.random.Random


class MainActivity : AppCompatActivity() {

    lateinit var myDb: DatabaseHelper

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val dateTime = Calendar.getInstance().time

        val dateFormatted = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(dateTime)

        val dateTextView : TextView = findViewById(R.id.textDate) as TextView
        dateTextView.setText(dateFormatted)

        myDb = DatabaseHelper(this)
        }


    fun secondActivity(view: View){
        val intent = Intent(this, passwords::class.java)
        startActivity(intent)
    }

    fun randomise(view: View){


        val passwordSet1 = Random.nextInt(0,99)
        val passwordSet2 = Random.nextInt(0,99)
        val passwordSet3 = Random.nextInt(0,99)
        val passwordSet4 = Random.nextInt(0,99)

        val editText1 = findViewById<EditText>(R.id.password1)
        editText1.setText(passwordSet1.toString())

        val editText2 = findViewById<EditText>(R.id.password2)
        editText2.setText(passwordSet2.toString())

        val editText3 = findViewById<EditText>(R.id.password3)
        editText3.setText(passwordSet3.toString())

        val editText4 = findViewById<EditText>(R.id.password4)
        editText4.setText(passwordSet4.toString())

    }



    fun save(view: View){
        var correct = true

        val description = descriptionName.text.toString().trim()
        val password = password1.text.toString().trim() + "-" + password2.text.toString().trim() + "-" + password3.text.toString().trim() + "-" + password4.text.toString().trim()
        val dateTime = Calendar.getInstance().time
        val dateFormatted = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(dateTime)

        if (TextUtils.isEmpty(description)){
            descriptionName.error = "Enter description"
            correct = false
        }

        if (password == "---"){
            password1.error = "Enter password"
            password2.error = "Enter password"
            password3.error = "Enter password"
            password4.error = "Enter password"
            correct = false
        }


        var isInserted = myDb.insertData(description, password, dateFormatted)


        if (isInserted == true && correct == true){
            Toast.makeText(this, "Data Saved", Toast.LENGTH_LONG).show()
            Toast.makeText(this, isInserted.toString(), Toast.LENGTH_LONG).show()
        } else {
            Toast.makeText(this, "Data Not Saved", Toast.LENGTH_LONG).show()
        }




    }

}

Here is the error that first appears when I try to start up the password activity

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.juliaojonah_hci_outcome2_v1, PID: 20054
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.juliaojonah_hci_outcome2_v1/com.example.juliaojonah_hci_outcome2_v1.passwords}: kotlin.UninitializedPropertyAccessException: lateinit property myDb has not been initialized
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2646)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2707)
        at android.app.ActivityThread.-wrap12(ActivityThread.java)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1460)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6077)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
     Caused by: kotlin.UninitializedPropertyAccessException: lateinit property myDb has not been initialized
        at com.example.juliaojonah_hci_outcome2_v1.passwords.onCreate(passwords.kt:42)
        at android.app.Activity.performCreate(Activity.java:6662)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2599)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2707) 
        at android.app.ActivityThread.-wrap12(ActivityThread.java) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1460) 
        at android.os.Handler.dispatchMessage(Handler.java:102) 
        at android.os.Looper.loop(Looper.java:154) 
        at android.app.ActivityThread.main(ActivityThread.java:6077) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756) 

This is what happens when I try to run the .getData function

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.juliaojonah_hci_outcome2_v1, PID: 20221
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.juliaojonah_hci_outcome2_v1/com.example.juliaojonah_hci_outcome2_v1.passwords}: kotlin.UninitializedPropertyAccessException: lateinit property myDb has not been initialized
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2646)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2707)
        at android.app.ActivityThread.-wrap12(ActivityThread.java)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1460)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6077)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
     Caused by: kotlin.UninitializedPropertyAccessException: lateinit property myDb has not been initialized
        at com.example.juliaojonah_hci_outcome2_v1.passwords.onCreate(passwords.kt:51)
        at android.app.Activity.performCreate(Activity.java:6662)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2599)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2707) 
        at android.app.ActivityThread.-wrap12(ActivityThread.java) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1460) 
        at android.os.Handler.dispatchMessage(Handler.java:102) 
        at android.os.Looper.loop(Looper.java:154) 
        at android.app.ActivityThread.main(ActivityThread.java:6077) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756) 

Solution

  • When I try using the functions the app crashes.

    lateinit property myDb has not been initialized is saying that you need to initialise myDB. that is you need to include :-

    myDB = DatabaseHelper(this)
    
    • before accessing/using myDB, perhaps straight after setContentView(R.layout.activity_main)

    I am using sqlite incorrectly in this kotlin class You aren't too far away but you appear to misunderstand queries and for Android Cursors. In short you are querying the database in a loop when a single query can return all of that information and you can then loop through the result the Cursor.

    Here's some of the issues.

    In the getData function you should be using "${COL_1}=?" for the 3rd paramter to the call to query as this parameter is the WHERE clause less the WHERE keyword. You are effectively saying SELECT whatever_column_is_passed FROM passwords_table WHERE x;

    • x will be the value passed (1 then 2 then 3 ....). As the where clause expects either true or false and that 0 is false and other numbers are positive all rows will be returned.

    However, you then have a few issues in regard to processing (not processing it) the Cursor returned from the getData.

    First you assume that the number of rows in the table val counter = myDb.getTableCount() is going to equate to the rows returned and that the id's will be numbered 1,2,3....... This may initially be the case but if a row is deleted or if an insert fails then the sequence may not be monotonically increasing.

    When going through the loop you are trying to assign a Cursor to a String e.g. var description = myDb.getData(id.toString(), "COL_2") will result in description being a Cursor. To get around the issue of trying to build a PasswordCluster not liking a Cursor you have used the Cursor's toString method which will result in the default object toString method being used, which returns details about the Cursos not the expected value (description, password or datetime).

    Suggested Fix

    It appears that you want to get a list of PasswordCluster's into passwords (val passwords = ArrayList<PasswordCluster>()) fro the RecyclerView.

    I'd suggest that you should have a function in the DatabaseHelper class that retruns the list of all passwords as an ArrayList e.g.

    fun getListOfAllPasswordClusters(): ArrayList<PasswordCluster> {
        val rv = ArrayList<PasswordCluster>()
        val db = this.writableDatabase
        val csr = db.query(TABLE_NAME,null /* ALL columns */,null,null,null,null,null)
        //return db.query("$TABLE_NAME", column, "$COL_1", columnValue, null, null, null )
        while (csr.moveToNext()) {
            rv.add(
                PasswordCluster(
                    csr.getString(csr.getColumnIndex(COL_2)),
                    csr.getString(csr.getColumnIndex(COL_3)),
                    csr.getString(csr.getColumnIndex(COL_4))
                )
            )
            /* NOTE ideally you should also store the id in PasswordCluster
                So assuming a constructor that takes
                    Long /* id */
                    ,String /* description */
                    ,String /* password */
                    ,String /* date_time */
    
                    E.G.
            rv.add(
                PasswordCluster(
                    csr.getLong(csr.getColumnIndex(COL_1)),
                    csr.getString(csr.getColumnIndex(COL_2)),
                    csr.getString(csr.getColumnIndex(COL_3)),
                    csr.getString(csr.getColumnIndex(COL_4))
                )
            )
            */
        }
        csr.close()
        return rv
    }
    
    • Note I strongly suggest ensuring that a PasswordCluster object can store the id as this value offers the most efficient means to access a specific row (e.g. to update/delete/extract a single row).
      • You will often see Int for id this will probably work in most cases but the id is actually a Long (64 bit signed integer). As such I suggest always considering it as a Long.
    • Coding AUTOINCREMENT is very likely not required and is also inefficient. Perhaps read SQLite Autoincrement.
      • It appears that many think that it's needed for getting an automatically generated sequential id, it's not INTEGER PRIMARY KEY on it's own does that.

    Then you could have :-

    class passwords : AppCompatActivity() {
    
        lateinit var myDb: DatabaseHelper
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_passwords)    
            val dateTime = Calendar.getInstance().time   
            val dateFormatted = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(dateTime)   
            val dateTextView : TextView = findViewById(R.id.textDate2) as TextView
            dateTextView.setText(dateFormatted)    
            val recyclerView = findViewById(R.id.savedPasswords) as RecyclerView    
            recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)    
            val passwords = getListOfAllPasswordClusters()
            val adapter = CustomAdapter(passwords)    
            recyclerView.adapter = adapter
        }
    
        fun firstActivity(view: View){
            val intent = Intent(this, MainActivity::class.java)
            startActivity(intent)
        }
    }
    

    Working example

    This is an example based solely upon adding the suggested getListOfAllPasswordClusters() (with the commented out part replacing the rv.add(.........) code so the id is retrieved) function and using the following code in the activity :-

    class MainActivity : AppCompatActivity() {
    
        lateinit var myDB: DatabaseHelper
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            myDB = DatabaseHelper(this)
            addSomeDataIfNone()
    
            val passwords = myDB.getListOfAllPasswordClusters()
            for (p: PasswordCluster in passwords) {
                Log.d("PASSWORDCLUSTER","ID=${p.id} Description=${p.description} password=${p.password} datetime=${p.date_time}")
            }
        }
    
        fun addSomeDataIfNone() {
            if (DatabaseUtils.queryNumEntries(myDB.writableDatabase,DatabaseHelper.TABLE_NAME) > 0) return
            myDB.insertData("Test1","password1","2019-12-01")
            myDB.insertData("Test2","password2","2019-12-02")
            myDB.insertData("Test3","password3","2019-12-03")
        }
    }
    
    • Note that for convenience the activity is MainActiviy rather than passwords

    The runs and results in :-

    2019-12-01 07:59:58.289 D/PASSWORDCLUSTER: ID=1 Description=Test1 password=password1 datetime=2019-12-01
    2019-12-01 07:59:58.289 D/PASSWORDCLUSTER: ID=2 Description=Test2 password=password2 datetime=2019-12-02
    2019-12-01 07:59:58.289 D/PASSWORDCLUSTER: ID=3 Description=Test3 password=password3 datetime=2019-12-03