This tutorial will introduce you to the use of SQLite databases within an Android application by building an application to track assignment deadlines
<ListView android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@+id/btnAddAssignment"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:id="@+id/listView" />
<Button android:text="Add Assignment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/btnAddAssignment"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@+id/listView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
addAssignment
method as follows:
fun addAssignment(){
}
onCreate
method set an onclickListener for the button which calles the addAssignment
methodbinding
for this in the usual way, note that the binding class will be called ActivityModuleListBindingbinding
addModule
.onCreate
set an onClickListener for the button to it calls the addModule
methodNext we'll create classes to represent the constructs of the Module and Assessment
class Module (val code: String, var name: String){
}
toString
method of Module so it displays the module code followed by a colon, a space and then the module title. Add this code to do that:
override fun toString(): String {
return "$code: $name"
}
import java.io.Serializable
class Module (val code: String, var name: String) : Comparable<Module>, Serializable {
override fun compareTo(other: Module): Int {
return code.compareTo(other.code)
}
override fun equals(other: Any?): Boolean {
if (other is Module){
return code.equals(other.code)
}
return super.equals(other)
}
override fun toString(): String {
return "$code: $name"
}
}
class Assignment(var id: Int?, var title:String, var weight: Int, var deadline: Date, var module:Module){
}
toString()
so that the deadline would appear in the following format dd/mm/yy CO5225 Assignment title (25%). Hint - format the date using an instance of the SimpleDateFormat classThe application we are building will persist data in a SQLite database on the Android Device (or emulator). Rather than include SQL statements througout the various Activities in the application, we will create a class with responsibility for creating, retrieving, updating and deleting data from the SQLite database, as well as creating the tables we need on first run.
class DataManager(context: Context) {}
private val db : SQLiteDatabase = context.openOrCreateDatabase("Assessment", Context.MODE_PRIVATE, null)
init
block to the code, beneath the above statement, and add the following code (which may be copy and pasted):
val modulesCreateQuery = "CREATE TABLE IF NOT EXISTS `Modules` ( `Code` TEXT NOT NULL, `Name` TEXT NOT NULL, PRIMARY KEY(`Code`) )"
val assignmentsCreateQuery = "CREATE TABLE IF NOT EXISTS `Assignments` ( `Id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `Title` TEXT NOT NULL, `Weight` INTEGER NOT NULL, `Deadline` INTEGER, `ModuleCode` TEXT )"
db.execSQL(modulesCreateQuery)
db.execSQL(assignmentsCreateQuery)
The first two statements are queries which will generate tables for the Modules and Assignments respectively when executed, and the second two lines execute the code. Take the time to read the code to understand the structure of the two tablesfun add(module: Module){
val query = "INSERT INTO Modules (code, name) VALUES ('${module.code}', '${module.name}')"
db.execSQL(query)
}
fun allModules() : List<Module>{
val modules = mutableListOf<Module>()
val cursor = db.rawQuery("SELECT * FROM Modules", null)
if (cursor.moveToFirst()) {
do {
val code = cursor.getString(cursor.getColumnIndexOrThrow("Code"))
val name = cursor.getString(cursor.getColumnIndexOrThrow("Name"))
val module = Module(code,name)
modules.add(module)
} while (cursor.moveToNext())
}
cursor.close()
return modules.sorted()
}
We will return to the class later to add more functionality, but for now we'll return to a couple of the activities within the app to try out the newly added methodsaddAssignment
method in MainActivity and create write code to create an Intent for the AssignmentDetailsActivity class, then start an Activity using the intent as the parameter.addModule
method, and add code (similar to that above) to navigate to the ModuleDetailActivityprivate fun validateModuleCode(code: String) : Boolean{
val regEx = "([A-Z]{2})([4-7])([0-9]{3})"
return code.matches(Regex(regEx))
}
private lateinit var dataManager : DataManager
onCreate
method (after the call to super) as follows:
dataManager = DataManager(this)
addModule
method to create a module object using the values in the editText elements and use the dataManager object to add it to the database:
val code = binding.etCode.text.toString()
if (validateModuleCode(code)) {
val module = Module(binding.etCode.text.toString(), binding.etTitle.text.toString())
dataManager.add(module)
finish()
}else {
binding.tvError.text = "Module code should be two upper case letters followed by four numbers"
}
Caused by: android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: Modules.Code (code 1555 SQLITE_CONSTRAINT_PRIMARYKEY)This appears because we cannot add the same module twice
private fun module(code: String) : Module? {
val query = "SELECT * FROM Modules WHERE Code='$code'"
val cursor = db.rawQuery(query,null)
return if (cursor.moveToFirst()) {
val name = cursor.getString(cursor.getColumnIndexOrThrow("Code"))
cursor.close()
Module(code,name)
} else {
cursor.close()
null
}
}
add(module: Module)
method, so it checks that the module doesn't already exist (by calling the method we just added, and checking the returned value is null) before trying to add itaddModule
method, and modify it so it appears as follows:
fun addModule(v: View){
val code = binding.etCode.text.toString()
if (validateModuleCode(code)) {
val module = Module(binding.etCode.text.toString(), binding.etTitle.text.toString())
if (dataManager.add(module)) {
finish()
}
else {
binding.tvError.text = "Unable to add module, does the module already exist?"
}
}else {
binding.tvError.text = "Invalid Module code"
}
}
Viewing the modules is not a primary aspect of the application, but it is necessary if the user wishes to modify them, and it would be useful to us to verify that the modules we've added are actually in the database. The list will be accessible via a menu on the MainActivity.
onCreateOptionsMenu
method as follows:
menuInflater.inflate(R.menu.menu_settings, menu)
return super.onCreateOptionsMenu(menu)
onOptionsItemSelected
method so that if the user selects the 'Edit Modules' option, we start the ModuleListActivity activity. the code to do this is as follows:override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menu_edit_modules){
val intent = Intent(this,ModuleListActivity::class.java)
startActivity(intent)
}
return super.onOptionsItemSelected(item)
}
private lateinit var modules : List
private lateinit var dataManager: DataManager
onCreate
instantiate the dataManager object (as you did in the ModuleDetailActivity class)private fun refreshList(){
modules = dataManager.allModules()
binding.listView.adapter = ArrayAdapter<Module>(this,android.R.layout.simple_list_item_1,modules)
}
onCreate()
In order that the user can select a module to which an assignment belongs, we will populate the spinner with a list of all modules
onCreate
methodprivate lateinit var modules : List<Module>
private fun refreshSpinner(){
modules = dataManager.allModules() //if you've used a different name for the dataManager, change the reference here (and in future code that uses this name)
val adapter = ArrayAdapter<Module>(this,android.R.layout.simple_spinner_item, modules)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spinner.adapter = adapter
}
onCreate
methodonResume
method and add a call to refreshSpinner()
Somewhat unhelpfully, the data picker object doesn't provide us with a Date object, instead, it gives us year, month and dayOfMonth values, from which we have to calculate a date. Add an extension method to your code (this can go anywhere outside of an existing class, but I would suggest a new Kotlin file called Extensions.kt would be a sensible location) as follows:
fun DatePicker.date() : Date{
val cal = Calendar.getInstance()
cal.set(year,month,dayOfMonth)
return cal.time
}
This will add a method to the CalendarPicker class which will return a Date object
fun add(assignment: Assignment) {
val query = """INSERT INTO Assignments (Title, Weight, Deadline, ModuleCode)
VALUES ('${assignment.title}', '${assignment.weight}', ${assignment.deadline.time}, '${assignment.module.code}')"""
db.execSQL(query)
}
addAssignment
methods
val title = binding.etTitle.text.toString()
val weight = binding.etWeight.text.toString().toInt()
val date = binding.datePicker.date()
val selectedModule = modules[binding.spinner.selectedItemPosition]
val assignment = Assignment(null, title, weight, date, selectedModule)
dataManager.add(assignment)
finish()
private fun assignments(query: String, args: Array<String>?) : List<Assignment>{
val assignments = mutableListOf<Assignment>()
val cursor = db.rawQuery(query,args)
if (cursor.moveToFirst()) {
do {
val id = cursor.getInt(cursor.getColumnIndexOrThrow("Id"))
val title = cursor.getString(cursor.getColumnIndexOrThrow("Title"))
val modCode = cursor.getString(cursor.getColumnIndexOrThrow("ModuleCode"))
val weight = cursor.getInt(cursor.getColumnIndexOrThrow("Weight"))
val dateLong = cursor.getLong(cursor.getColumnIndexOrThrow("Deadline"))
val date = Date(dateLong)
val assignment = Assignment(id, title, weight, date, module(modCode)!!)
assignments.add(assignment)
} while (cursor.moveToNext())
}
cursor.close()
return assignments.sorted()
}
fun assignments() : List<Assignment>{
val query = "SELECT * FROM Assignments"
return assignments(query, null)
}
onCreate
private lateinit var dataSet : List<Assignment>
private fun refreshDeadlines(){
dataSet = dataManager.assignments()
binding.listView.adapter = ArrayAdapter<Assignment>(this,android.R.layout.simple_list_item_1,dataSet)
}
onResume
and add a call to refreshDeadlines
there alsoonCreateContextMenu
method and inflate the 'menu_deadlines_context' menu you created earlieronCreate
add the following code so the ListView can use the context menu:
registerForContextMenu(binding.listView)
fun delete(assignment: Assignment){
if (assignment.id != null) {
val whereClause = "Id = ?"
val whereArgs = arrayOf(assignment.id.toString())
db.delete("Assignments", whereClause,whereArgs )
}
}
Note that parameters are represented by questions marks ?
in the where clause, and parameters are provided as a list of stringsonContextItemSelected
method, with the following implementation:
if (item.itemId == R.id.menu_delete){
val info = item.menuInfo as AdapterView.AdapterContextMenuInfo
val component = dataSet[info.position]
dataManager.delete(component)
refreshDeadlines()
return true
}
return super.onContextItemSelected(item)
private fun assignmentsForModule(module: Module) : List<Assignment>{
val query = "SELECT * FROM Assignments WHERE ModuleCode = ?"
return assignments(query, arrayOf(module.code))
}
fun delete(module: Module){
//check for assignments and delete these first
val moduleAssignments = assignmentsForModule(module)
for (assignment in moduleAssignments){
delete(assignment)
}
db.delete("Modules","Code = ?",arrayOf(module.code.toString()))
}
fun update(module:Module){
val contentValues = ContentValues()
contentValues.put("Name", module.name)
val args = arrayOf(module.code)
db.update("Modules",contentValues,"Code = ?", args)
}
private var existingModule : Module? = null
onCreate
method:
val module = intent.getSerializableExtra("module")
if (module is Module) {
existingModule = module
binding.etTitle.setText(module.name)
binding.etCode.setText(module.code)
bindinng.etCode.isEnabled = false //can't change code as primary key - delete module instead
binding.button.text = "Update"
}
existingModule
is an ivar, so could be modified by something else. Instead we will take a copy and use that. Add the following code to the existing addModule
method, such that all the code currently within the method moves into the else
block, as denoted by the comment:
val immutableExistingModule = existingModule
if (immutableExistingModule != null){
immutableExistingModule.name = binding.etTitle.text.toString()
dataManager.update(immutableExistingModule)
finish()
} else {
//existing code should go here
}
onCreate
method so that the user can view the details of a module when he/she taps on one:
listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
val module = modules[position]
val intent = Intent(this,ModuleDetailActivity::class.java)
intent.putExtra("module",module)
startActivity(intent)
}
onResume
for the ModuleListActivity so that the changes to a module name update immediatelyval cal = Calendar.getInstance()
cal.time = assignment.deadline
datePicker.init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), null)
setSelection()
method however, as this requires an index value, you will need to find out how to find the index of the assignment's module in the assignments
ArrayList.A completed version of this project can be found on GitHub
execSQL()
method. Modify the code to use the update()
methods refreshDeadlines
method for changes to the data set (e.g. following deletion) is sub-optimal. Modify the code to hand deletions and edits more eloquently