Step 13 of 13
Build a task manager CLI with data classes, collections, serialization, file I/O, and tests
Workshop สุดท้าย — สร้าง Task Manager CLI ที่รวมทุกสิ่งที่เรียนมา
Build a command-line task manager that uses everything you learned: data classes, collections, functions, error handling, and testing. This is the final workshop for the Kotlin Fundamentals roadmap.
A CLI app that lets you:
Create a new Kotlin/JVM project in IntelliJ IDEA. Add these dependencies:
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.10"
kotlin("plugin.serialization") version "2.4.10"
application
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
testImplementation(kotlin("test"))
}
application {
mainClass.set("TaskManagerKt")
}
import kotlinx.serialization.Serializable
@Serializable
data class Task(
val id: Int,
val title: String,
val done: Boolean = false,
)
import kotlinx.serialization.json.Json
import java.io.File
class TaskStore(private val filePath: String = "tasks.json") {
private val json = Json { prettyPrint = true }
private var tasks: MutableList<Task> = load()
private fun load(): MutableList<Task> {
val file = File(filePath)
if (!file.exists()) return mutableListOf()
return json.decodeFromString(file.readText())
}
fun save() {
File(filePath).writeText(json.encodeToString(tasks))
}
fun add(title: String): Task {
val id = (tasks.maxOfOrNull { it.id } ?: 0) + 1
val task = Task(id = id, title = title)
tasks.add(task)
save()
return task
}
fun all(): List<Task> = tasks.toList()
fun markDone(id: Int): Boolean {
val index = tasks.indexOfFirst { it.id == id }
if (index == -1) return false
tasks[index] = tasks[index].copy(done = true)
save()
return true
}
fun delete(id: Int): Boolean {
val removed = tasks.removeIf { it.id == id }
if (removed) save()
return removed
}
}
fun main(args: Array<String>) {
val store = TaskStore()
val command = args.firstOrNull() ?: "help"
val arguments = args.drop(1)
when (command) {
"add" -> {
val title = arguments.joinToString(" ")
if (title.isBlank()) {
println("Usage: add <task title>")
} else {
val task = store.add(title)
println("Added: [${task.id}] ${task.title}")
}
}
"list" -> {
val tasks = store.all()
if (tasks.isEmpty()) {
println("No tasks yet.")
} else {
tasks.forEach { task ->
val status = if (task.done) "[x]" else "[ ]"
println("${task.id}. $status ${task.title}")
}
}
}
"done" -> {
val id = arguments.firstOrNull()?.toIntOrNull()
if (id == null) {
println("Usage: done <task id>")
} else if (store.markDone(id)) {
println("Marked task $id as done.")
} else {
println("Task $id not found.")
}
}
"delete" -> {
val id = arguments.firstOrNull()?.toIntOrNull()
if (id == null) {
println("Usage: delete <task id>")
} else if (store.delete(id)) {
println("Deleted task $id.")
} else {
println("Task $id not found.")
}
}
"help" -> {
println("""
Task Manager — Commands:
add <title> Add a new task
list Show all tasks
done <id> Mark a task as done
delete <id> Delete a task
help Show this message
""".trimIndent())
}
else -> {
println("Unknown command: $command")
println("Type 'help' for available commands.")
}
}
}
./gradlew run --args="add Buy groceries"
./gradlew run --args="add Learn Kotlin coroutines"
./gradlew run --args="list"
# 1. [ ] Buy groceries
# 2. [ ] Learn Kotlin coroutines
./gradlew run --args="done 1"
./gradlew run --args="list"
# 1. [x] Buy groceries
# 2. [ ] Learn Kotlin coroutines
./gradlew run --args="delete 2"
./gradlew run --args="list"
# 1. [x] Buy groceries
import kotlin.test.*
class TaskStoreTest {
private fun createStore(): TaskStore {
return TaskStore("test-tasks.json")
}
@AfterTest
fun cleanup() {
File("test-tasks.json").delete()
}
@Test
fun `add creates task with auto-incremented id`() {
val store = createStore()
val task1 = store.add("First")
val task2 = store.add("Second")
assertEquals(1, task1.id)
assertEquals(2, task2.id)
}
@Test
fun `markDone sets done to true`() {
val store = createStore()
val task = store.add("My task")
val result = store.markDone(task.id)
assertTrue(result)
assertTrue(store.all().first().done)
}
@Test
fun `markDone returns false for missing id`() {
val store = createStore()
assertFalse(store.markDone(999))
}
@Test
fun `delete removes task`() {
val store = createStore()
val task = store.add("To delete")
assertTrue(store.delete(task.id))
assertTrue(store.all().isEmpty())
}
}
Try these to deepen your skills:
priority field (LOW, MEDIUM, HIGH) and sort tasks by prioritydueDate field using kotlinx-datetime and show overdue taskscategory field and a list --category <name> commandsearch <keyword> command using filterexport command that writes tasks to CSV using joinToStringundo command| Feature | Where you used it |
|---|---|
| Data classes | Task model with @Serializable |
| Collections | MutableList, filter, maxOfOrNull |
when expressions | CLI command dispatch |
| Null safety | firstOrNull(), toIntOrNull(), ?. |
| File I/O | File.readText(), File.writeText() |
| Serialization | Json.encodeToString, Json.decodeFromString |
| Testing | TaskStoreTest with lifecycle, assertions |
| Error handling | Invalid input, missing task IDs |
Previous
Coding Conventions and Detekt
Final step