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
- PROGRAMMING LANGUAGE - Kotlin
- DESIGN PATTERN - MVVM(Model View ViewModel)
- DATABASE - SQLite using Room.
- APP FLOW - Single Page Design
- LISTINGS VIEW - ExpandableRecyclerView with sections
- LISTINGS STYLE - Daily View and Archive View
- INPUT VIEW - Material Lovely Input Dialogs
- BRANDING - Animated Splash screen, Custom Fonts
- 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:
- ColumnInfo - allows us to specify meta data about the columns to be generated by room
- Entity - allows us to mark a class as a candidate for table generation. It also allows us specify the table name.
- 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:
- Id
- Task string
- Task date
- 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:
- 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.
- Dao - Data Access Object. Marks our interface as Dao.
- Delete - Allows us mark our delete method.
- Insert - allows us mark our insert method.
- Update - allows us mark our Update method.
- Query - allows us mark our select method
- 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.
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.
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:
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
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.
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:
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:
We then declare our TaskDao
:
Then come instantiate our Room database and initialize our 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:
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:
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:
- Header part of section
- Content part of section.
Our section will have two states:
- Expanded state
- Collapsed state.
We will receive the section name as a parameter in our constructor then make the recyclerview expandable by default.
Download
Download the app here.