Android Jetpack Compose - Creating A Simple Todo

Introduction

Hello! 😎 In this tutorial I will show you how to create a simple Todo application using Android Jetpack Compose and using Jetpack's Room library.


Creating The Project

First we need to actually create the project, fire up android studio and create a new compose project:

New Project

Then give the new project a name like "Jetpack Room". Once done click on Finish.

New Project Name


Installing The Dependencies

Next we need to install the dependencies, open up your app's build.gradle file and add the following:

implementation 'androidx.room:room-runtime:2.5.0'
kapt 'androidx.room:room-compiler:2.5.0'

Because we are using the kapt plugin we will also need to add this to the list of plugins in the same file like so:

plugins {
	id 'kotlin-kapt'
}

Next Sync the project and then create a new "MainApplication" and add the following like below the application block like so:

<application
	android:name=".MainApplication"	
>
</application>

Now that we have the project set up we can finally start coding. πŸ˜„


Creating The Room Database

First we will be creating the database in order to store todos on the user's phone, this will make it so that the todos aren't deleted when the user destroys the application.

If you didn't know Room provides an abstraction layer over SQLite to allow for a more robust database access while harnessing the full power of SQLite.

Create a new "db" package to handle the Database operations and create a new file called "AppDatabase" and fill the file with the following:

package com.example.jetpackroom.db

import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters

@Database(entities = [Todo::class], version = 1, exportSchema = false)
@TypeConverters(DateTimeConverter::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun todoDao(): TodoDao

    companion object {
        const val NAME = "todos"
    }
}

Here we create a Room Database object, don't worrying about the class doesn't exist errors etc as we will be creating them as well. The database will manage a Todo class, with a version of one and since we don't really need to export the schema we also set "exportSchema" to false.

We also use a TypeConverter to handle dates, (Room doesn't handle dates very well).

We also finally name the database "todos".

Next we need to create the TypeConverter so create a new file called "DateTimeConverter" and flesh it with the following:

package com.example.jetpackroom.db

import androidx.room.TypeConverter
import java.util.*

class DateTimeConverter {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time?.toLong()
    }
}

In this class we provide a way for Room to handle dates, here we just give two methods, one to get the date from the timestamp and one to convert the date back to timestamp.

Next we need to create the Todo entity that we provided to the AppDatabase, open up another file called "Todo" and fill it with the following:

package com.example.jetpackroom.db

import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.Date

@Entity(tableName = "todos")
data class Todo(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val title: String,
    val created_at: Date = Date()
)

Here all of the previous errors should have gone away. Here we set the Entity table name to "todos", it has a primary key of id which auto increments, a title and when it was created. Pretty simple Todo.

Next we need a DAO (Data Access Object) which will enable us to query the database. Create a new file called "TodoDao" and fill it with the following contents:

package com.example.jetpackroom.db

import androidx.room.*

@Dao
interface TodoDao {
    @Query("select * from todos order by created_at asc")
    fun getAll(): MutableList<Todo>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun post(todo: Todo)

    @Delete
    fun delete(todo: Todo)
}

This class will allow us to communicate with the database, here we have three methods, one for getting all of the Todos from the database, one to insert into the database if there is a conflict we just replace the old todo and finally one method to delete the todo from the database.

Now that we have the database side of things sorted what's the point of having it if you don't use it. ☺️


Create A Simple Todo Application

Finally we now get to use the code we coding in the previous section. πŸ˜†

Open up "MainActivity" and fill it with the following:

package com.example.jetpackroom

import android.app.Application
import androidx.room.Room
import com.example.jetpackroom.db.AppDatabase

class MainApplication : Application() {
    companion object {
        lateinit var database: AppDatabase
    }

    override fun onCreate() {
        super.onCreate()

        database = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java,
            AppDatabase.NAME
        ).build()
    }
}

Here we basically use the MainApplication class to initialize the database object.

Finally all we need to do now is edit "MainActivity", open it up and change it to the following:

package com.example.jetpackroom

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.example.jetpackroom.db.Todo
import com.example.jetpackroom.ui.theme.JetpackRoomTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity : ComponentActivity() {
    private val dao = MainApplication.database.todoDao()
    private var todoList = mutableStateListOf<Todo>()
    private var scope = MainScope()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetpackRoomTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    MainScreen(todoList = todoList)
                    loadToDo()
                }
            }
        }
    }

    private fun loadToDo() {
        scope.launch {
            withContext(Dispatchers.Default) {
                dao.getAll().forEach { todo ->
                    todoList.add(todo)
                }
            }
        }
    }

    private fun postTodo(title: String) {
        scope.launch {
            withContext(Dispatchers.Default) {
                dao.post(Todo(title = title))

                todoList.clear()
                loadToDo()
            }
        }
    }

    private fun deleteTodo(todo: Todo) {
        scope.launch {
            withContext(Dispatchers.Default) {
                dao.delete(todo)

                todoList.clear()
                loadToDo()
            }
        }
    }

    @OptIn(ExperimentalComposeUiApi::class)
    @Composable
    fun MainScreen(todoList: SnapshotStateList<Todo>) {
        val context = LocalContext.current
        val keyboardController: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current
        var text: String by remember {
            mutableStateOf("")
        }

        Column(
            modifier = Modifier.clickable {
                keyboardController?.hide()
            }
        ) {
            TopAppBar(
                title = { Text(text = stringResource(id = R.string.main_title)) }
            )
            LazyColumn(modifier = Modifier
                .fillMaxWidth()
                .weight(1f)) {

                items(todoList) { todo ->
                    key(todo.id) {
                        TodoItem(
                            todo = todo,
                            onClick = {
                                deleteTodo(todo)
                            }
                        )
                    }
                }
            }
            Row(modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)) {

                OutlinedTextField(
                    value = text,
                    onValueChange = { text = it },
                    colors = TextFieldDefaults.textFieldColors(
                        backgroundColor = Color.Transparent,
                        focusedIndicatorColor = Color.Transparent,
                        unfocusedIndicatorColor = Color.Transparent
                    ),
                    modifier = Modifier.border(
                        BorderStroke(2.dp, Color.Gray)
                    ),
                    label = { Text(text = stringResource(id = R.string.main_new_todo)) }
                )

                Spacer(modifier = Modifier.size(16.dp))

                Button(
                    onClick = {
                        if (text.isEmpty()) return@Button

                        postTodo(text)
                        text = ""
                    },
                    modifier = Modifier.align(Alignment.CenterVertically)
                ) {
                    Text(text = stringResource(id = R.string.main_add_todo))
                }
            }
        }
    }
}

This UI is pretty simple it shows a list of all the todo objects, and provided a text area that allows you to input text, once done click the Add button to add the todo to the list and database. You can also click on the object to delete it.

Also note the todo persists even if the user restarts the application.

Now we have a simple Todo application. πŸ˜ƒ

Todo App


Conclusion

Here I have shown how to create a simple todo application using Android Jetpack Compose, I hoped this has helped you. 😁

Feel free to try and implementing todo updating or try playing around with the UI.

Heppy Coding! 😎

As always you find the repository via: https://github.com/ethand91/android-compose-todo


Like me work? I post about a variety of topics, if you would like to see more please like and follow me. Also I love coffee.

β€œBuy Me A Coffee”

If you are looking to learn Algorithm Patterns to ace the coding interview I recommend the following course