Skip to content

Daily Tasks Planner App – Kotlin + Room CRUD+MVVM

Tasks Planner is a beautiful android app project meant to teach student several modern technologies with regards to android development. It is designed as a learning project but can be customized and uploaded to play store.

The app allows users to plan tasks. It's a beautiful app written in Kotlin.

Concepts You will learn

Here are the things you will learn from this project

  1. PROGRAMMING LANGUAGE - Kotlin
  2. DESIGN PATTERN - MVVM(Model View ViewModel)
  3. DATABASE - SQLite using Room.
  4. APP FLOW - Single Page Design
  5. LISTINGS VIEW - ExpandableRecyclerView with sections
  6. LISTINGS STYLE - Daily View and Archive View
  7. INPUT VIEW - Material Lovely Input Dialogs
  8. BRANDING - Animated Splash screen, Custom Fonts
  9. RECYCLERVIEW ACTIONS - Swipe recyclerview to reveal buttons

If you want to launch your first app without much hustle then you have a perfect template here.

What is Room?

Room is a data access layer that simplifies the process of working with SQLite database. Through Room we don't have to write complex SQL statements to perform our CRUD operations. Room can generate these on our behalf just by using simple attributes in our code.

Programmatically Defining a Task

Well we are creating a Tasks Planner app. The first step is to define what we mean by Task. Basically we create a data object class that models our Task.

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

import java.io.Serializable

@Entity(tableName = "tasksTB")
class Task : Serializable {

    @ColumnInfo(name = "id")
    @PrimaryKey(autoGenerate = true)
    var id: Int = 0
    @ColumnInfo(name = "task")
    var task: String? = null
    @ColumnInfo(name = "taskDate")
    var task#date: String? = null
    @ColumnInfo(name = "taskStatus")
    var taskStatus: String? = null
}
//end

In the above code we started by importing three room attributes:

  1. ColumnInfo - allows us to specify meta data about the columns to be generated by room
  2. Entity - allows us to mark a class as a candidate for table generation. It also allows us specify the table name.
  3. PrimaryKey - allows us specify our primary key. If the primary key is an integer, Room can autogenerate it for you. If it is a string you have to supply the key yourself.

The properties of our Task therefore include:

  1. Id
  2. Task string
  3. Task date
  4. Task status

Creating our Data Access Object interface

Here we will start by adding our imports:

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import info.camposha.tasksplanner.data.model.Task

Here are what those imports do for us:

  1. LiveData - Our select methods will return these. They allow us to reactively pass our data to the caller. The caller can then subscribe to receive updates on the data.
  2. Dao - Data Access Object. Marks our interface as Dao.
  3. Delete - Allows us mark our delete method.
  4. Insert - allows us mark our insert method.
  5. Update - allows us mark our Update method.
  6. Query - allows us mark our select method
  7. OnConflictStategy - allows us to specify what to do when a conflict occurs during an operation attempt

We then define our interface and decorate with the @Dao attribute. Note that an interface can have only abstract methods. Thus all methods here will be abstract.

@Dao
interface TaskDAO {

Then we come define our CRUD methods

    //NB= Methods annotated with @Insert can return either void, long, Long, long[],
    //Long[] or List<Long>.
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(task: Task): Long

The above method will allow us perform an insert into our SQLite database using room. If a conflict occurs then we will replace the existing row with the new data. You can see we have specified this strategy using the OnConflictStategy statement. Our insert method in this case is returning a Long object. However it can alsi return void,long,Long[] or List.

Then we have our update method:

    //Update methods must either return void or return int (the number of updated rows).
    @Update(onConflict = OnConflictStrategy.REPLACE)
    fun update(task: Task): Int

The method allows us to update a row. An integer representing the number of updated rows is returned.

Then a method to allow us insert all tasks in a list.

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insertAll(todos: List<Task>): Array<Long>

The above method allows us insert many items at once. If a conflict occurs we ignore it.

Then a method to allow us delete a row.

    //Deletion methods must either return void or return int (the number of deleted rows).
    @Delete
    fun delete(task: Task): Int

We also have query methods. The below method allows us select all tasks from our database:

    @Query("SELECT * FROM tasksTB")
    fun selectAll(): LiveData<List<Task>>

You can see we are returning a LiveData. Subscribers will observe that live data and obtain the List of tasks.

Archive Page - Grouped by Dates

Archive Page - Grouped by Dates

If you want to filter our items based on the date added, here is the statement:

    @Query("SELECT * FROM tasksTB WHERE taskDate LIKE :taskDate")
    fun selectByDate(task#date: String): LiveData<List<Task>>

If you want to filter items based on the status of the task, whether it is complete or not:

    @Query("SELECT * FROM tasksTB WHERE taskStatus LIKE :status")
    fun selectByStatus(status: String): LiveData<List<Task>>

You use a query also if you intend to delete a single item. The query allows us to filter the row to be deleted.

    @Query("DELETE FROM tasksTB WHERE id LIKE :id")
    fun delete(id: Int): Int

You can also delete all rows:

    //NB= Deletion methods must either return void or return int (the num of deleted rows).
    @Query("delete from tasksTB")
    fun deleteAll(): Int
}
//end

Creating our Room Database

We will need to create an abstract special class to represent our Room database. In reality our database is actually an sqlite database. However Room is an abstraction layer we are using in top of sqlite.

Start by creating an abstract class extending RoomDatabase:

@Database(entities = [Task::class], version = 2, exportSchema = false)
abstract class MyRoomDB : RoomDatabase() {

We have specified the entities, version and exportShema properties. You have to specify te entities as they represent the tables.

Then in our abstract class we will have one abstract method:

    abstract fun taskDAO(): TaskDAO

Then in our companion object we will instantiate our MyRoomDB:

    companion object {
        //Will hold our Room Database reference which we can then reuse
        private var myRoomDB: MyRoomDB? = null

        fun getInstance(context: Context): MyRoomDB {
            if (myRoomDB == null) {
                //Let's build our RoomDatabase using builder pattern
                myRoomDB = Room.databaseBuilder(
                    context, MyRoomDB::class.java,
                    "MyRoomDatabase"
                )
                    .fallbackToDestructiveMigration()
                    .build()
            }
            return myRoomDB as MyRoomDB
        }
    }

Actually Performing the CRUD operations

Well we will actually perform the CRUD operations in our TasksRepository class. We will be doing these operations in the background thread using asynctask class.

Our TaskRepository class will receive a Context as a parameter:

class TaskRepository(context: Context) {

We then declare our TaskDao:

    companion object {
        private lateinit var taskDAO: TaskDAO
    }

Then come instantiate our Room database and initialize our TaskDao:

    init {
        val myRoomDB = MyRoomDB.getInstance(context)
        taskDAO = myRoomDB.taskDAO()
    }

Because we don't want to feeze our user interface while accessing SQLite database, we will do all our operations in the background thread. For example to create a task we start by defining our asynctask class:

    internal class CreateTaskTask : AsyncTask<Task, Void, Long>() {
        override fun doInBackground(vararg tasks: Task): Long? {
            return taskDAO.insert(tasks[0])
        }
    }

Then a public method to execute our asynctask:

    fun createTask(task: Task): Long? {
        try {
            return CreateTaskTask().execute(task).get()
        } catch (e: ExecutionException) {
            e.printStackTrace()
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }

        return null
    }

We do the same thing for all our CRUD methods.

Exposing Functionality using our ViewModel class

We will now create a view model class. This class will expose the functionalities we had defined in our Task repository class.

Create it by extending the AndroidViewModel class. A mandatory Application object will be passed as a parameter via the constructor:

class TaskViewModel(application: Application) : AndroidViewModel(application) {

We will then initialize our instance fields:

    private val taskRepository: TaskRepository
    private var mLiveData: LiveData<List<Task>>? = null
    private val context: Context

    init {
        this.context = application
        taskRepository = TaskRepository(context)
    }

Here is the property that will expose our data to the UI:

    val getAll: LiveData<List<Task>>?
        get() {
            mLiveData = taskRepository.getAllTasks()
            return mLiveData
        }

Here is the method that will expose our Create task functionality we had defined in our Task repository:

    fun create(task: Task): Long? {
        return taskRepository.createTask(task)
    }

Then we have also methods to expose the update and delete functionality:

    fun update(task: Task): Int? {
        return taskRepository.updateTask(task)
    }

    fun delete(task: Task): Int? {
        return taskRepository.deleteChore(task)
    }

Programmatically defining a section

We also need to define a class to represent our recyclerview section. We are creating an expandable sectioned recyclerview. This type of recyclerview normally has two view types:

  1. Header part of section
  2. Content part of section.

Our section will have two states:

  1. Expanded state
  2. Collapsed state.
class Section(val name: String) {
    var isExpanded: Boolean = false

    init {
        isExpanded = true
    }
}

We will receive the section name as a parameter in our constructor then make the recyclerview expandable by default.

Download

Download the app here.