At this stage the first major functionality of the app has been implemented.
With this devlog I cover how I added data persistence of the tasks as well as the “Add Task” functionality. I used the Room library that comes as part of Android Jetpack for expediency, but nonetheless I had some stumbling blocks and learning points along the way. Regardless, I would highly recommend using Room so as to not get bogged down with how to query data from the SQLite database and increasing your chances of mucking something up “accidentally”.
Below you can see the results of adding a task to the app.
Getting Roomy
‘… nonetheless I had some stumbling blocks and learning points along the way’
- me, earlier
I found implementing Room deceptively straightforward since I add only had to add annotations to my already existing Task object like so:
@Entity (tableName = "tasks")
data class Task(@PrimaryKey(autoGenerate = true) var uid :Int,
@ColumnInfo(name = "task_name") var name:String,
@ColumnInfo(name = "task_duration") var duration:Float)
Then I needed only to make a data access object (for interfacing with the database) and extend the Room database into a TaskDatabase. The task data access object (DAO) contains the query annotations for accessing the database in specific, controlled ways. For example, I wanted to be able to pull all the tasks in the task database at once into a MutableList which meant that one of the functions looked something like this:
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks")
fun getAll(): MutableList<Task>
//Other functions here...
After that I thought I was all set, but alas it was not to be. Turns out I ran into two different compile-time problems: one regarding my annotations, and the other when trying to build the database instance at runtime.
The first error was that my annotations were not generating the correct classes at compile time, and so the compiler would complain that my abstract TaskDatabase class was being used without an implementation. This was caused by my inability to heed the following warning at the bottom of the documentation:
The second error was that when I was building my database as a top level member of the MainActivity, I needed to pass the applicationContext somehow, but the problem was that the applicationContext did not yet exist when I tried to build the database using:
val db = Room.databaseBuilder(
context.applicationContext,
TaskDatabase::class.java, "taskdb").build()
My aim was to have one db instance that I could then access the data from repeatedly instead of creating a new TaskDatabase instance everytime I wanted to access data. Enter the companion object.
In Kotlin, a companion object is a singleton tied to the class rather than the instances of that class. This meant that I can declare a companion object for the TaskDatabase and only need to pass the applicationContext when I want to get the singleton instance. So I added the following to the TaskDatabase:
companion object {
fun getInstance(context: Context): TaskDatabase =
Room.databaseBuilder(context.applicationContext,
TaskDatabase::class.java, "taskdb").build()
}
Thereafter, I could access the instance of the database easily (and more cleanly) with the following:
db = TaskDatabase.getInstance(this)
In the line above, this is the activity context itself from which we extract the applicationContext.
With this done, I thought it would be smooth sailing from here on out, but yet again…
No stop UI pls
Android has a strict (and good) policy of not allowing anything to block the UI thread which is the main/default thread in an activity. This helps make sure that the app UI stays responsive while you try to, say, fetch tasks from a database.
Although my app would compile and launch, it would be killed when I tried to load the tasks from the database because the UI thread would block for a second and Android would think my app was being unresponsive and kill it. To fix this issue I would need to offload my data loading into a non-UI worker thread. Thank goodness for AsyncTasks.
AsyncTasks provide an easy way to use Thread and Handlers in order to complete short tasks (for long running tasks you should be looking at Services). So, for example, I wanted to fetch all the tasks from the database and return it in a MutableList. For this I extended AsyncTask and created TaskFetchAsync with the following:
class TaskFetchAsync (val db: TaskDatabase) : AsyncTask<Void, Void, MutableList<Task>> (){
override fun doInBackground(vararg p0: Void?): MutableList<Task>? {
val taskList = db.taskDao().getAll()
return taskList
}
override fun onPostExecute(result: MutableList<Task>?) {
super.onPostExecute(result)
}
}
In the class declaration, you can see that the TaskFetchAsync takes a database instance in the constructor, while we also mention that it returns a MutableList containing Task objects.
Next, we fetch the tasks using our database instance’s taskDao and finally return the result. Using the AsyncTask was really straightforward since all we would need to do is:
taskMutableList.addAll(TaskFetchASync(db).execute().get())
That said, one thing I did do was give the db instance as a constructor
variable instead of declaring it as a parameter. This is because the parameters
(taken in whenever TaskFetchAsync.execute()
is called are generally something
that needs to be acted on, or may change the task execution in some way. This
became evident when I created another AsyncTask for inserting new tasks into
the database.
Insert A Sink
The final hurdle was to complete the functionality of the AddTask activity. On the MainActivity, when the “Add Task” button is pressed it should take the user to the AddTask activity where they can enter the new task information. If the user then saves the new task to the database it should notify the listViewAdapter I mentioned in my previous devlog to update the ListView with our newly added task.
In order to save the new task to the database I also used an AsyncTask that I called TaskInsertAsync that looked like the following:
class TaskInsertAsync(val db: TaskDatabase) : AsyncTask<Task, Void, Boolean> (){
override fun doInBackground(vararg p0: Task): Boolean {
db.taskDao().insertAll(*p0)
return true
}
// Other stuff...
}
What is notable here is that taskDao().insertAll()
takes vararg task: Task
which p0
is. Unfortunately, p0
is actually viewed as an array, while the
method is looking for a distinct, separate Task objects to pass to
insertAll()
and so we needed to use Kotlin’s spread
operator(*).
Finally came the question of how to know when a new task was added successfully. Enter startActivityForResult.
With startActivityForResult, we needed to declare a request code (a simple Int)
and then implement onActivityResult
to handle the result. This was simple
enough and ended up looking something like this:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent)
{
if (requestCode == ADD_TASK_REQUEST)
{
if (resultCode == Activity.RESULT_OK)
{
taskMutableList.clear()
taskMutableList.addAll(TaskFetchASync(db).execute().get())
listViewAdapter.notifyDataSetChanged()
}
}
}
It is to be noted that our AddTask activity did not need to handle the request code. Only our MainActivity had the request code declared and would handle it when the activity returned.
With all that done, the app could now persist data, fetch it, and add new tasks to the database. It also represents the first “real” functionality that is user-facing and so is a milestone I was pretty excited by!
As always, I will be back with another devlog describing all the times I shot myself in the foot and how I then fixed it. I shall leave you, the reader, with the following quote:
‘Give a man a program, frustrate him for a day. Teach a man to program, frustrate him for a lifetime.’
- Muhammad Waseem