← thecodex.expert · The Codex Family of Knowledge
Tier 1 · Beginner · Kotlin Project

To-Do List

Add, view, and remove tasks — and save them so they are still there next time. Your first app that remembers things between runs.

🧠 Teaches how to think spoonfed, every age Last verified:

1 The Problem

We want a to-do list you can actually use: add tasks, see them numbered, remove the ones you finish, and — crucially — have them saved to a file so they survive after you close the program. It teaches lists, a menu loop, and saving data to disk.

Where this shows up: every app that stores your stuff — notes, reminders, shopping lists, saved games, settings. The pattern of “keep a list in memory, save it to a file, load it back next time” is the simplest form of a database.

2 How to Think About It

Think about the loop the program lives in, before any code:

The plan — in plain English
1. Load any saved tasks from a file when the program starts. → 2. Show a menu (add / view / remove / quit) and repeat until they quit. → 3. Each choice changes the list. → 4. Save the list back to the file whenever it changes, so nothing is lost.

Add

View

Remove

Quit

Load tasks from file

Show menu

Choice?

Add task

Show tasks

Remove task

Stop

Save to file

3 The Build — explained part by part

Here is the complete to-do list. Read each part’s note below — you should understand the whole thing from the notes alone.

Kotlintodo.kt
import java.io.File

private val FILE = File("tasks.txt")

/** One task: done or not, plus its text. Stored as "0|text" / "1|text" per line. */
data class Task(val done: Boolean, val text: String) {
    fun toLine(): String = (if (done) "1" else "0") + "|" + text

    companion object {
        fun fromLine(line: String): Task {
            val (flag, text) = line.split("|", limit = 2)
            return Task(flag == "1", text)
        }
    }
}

/** If a saved file exists, read the tasks from it; otherwise start empty. */
fun loadTasks(): MutableList<Task> =
    if (FILE.exists())
        FILE.readLines().filter { it.isNotBlank() }.map { Task.fromLine(it) }.toMutableList()
    else
        mutableListOf()

/** Writes the tasks to the file so they survive after the program closes. */
fun saveTasks(tasks: List<Task>) {
    FILE.writeText(tasks.joinToString("\n") { it.toLine() })
}

fun addTask(tasks: MutableList<Task>, text: String): MutableList<Task> {
    tasks.add(Task(false, text))
    return tasks
}

fun removeTask(tasks: MutableList<Task>, number: Int): MutableList<Task> {
    if (number in 1..tasks.size) tasks.removeAt(number - 1)
    return tasks
}

fun main() {
    val tasks = loadTasks()

    while (true) {
        println("\n1. Add  2. View  3. Remove  4. Quit")
        print("Choose: ")
        val choice = readLine()?.trim() ?: break

        when (choice) {
            "1" -> {
                print("New task: ")
                val text = readLine() ?: ""
                addTask(tasks, text)
                saveTasks(tasks)
            }
            "2" -> tasks.forEachIndexed { i, t ->
                println("${i + 1}. [${if (t.done) "x" else " "}] ${t.text}")
            }
            "3" -> {
                print("Remove which number? ")
                val number = readLine()?.trim()?.toIntOrNull()
                if (number != null && number in 1..tasks.size) {
                    removeTask(tasks, number)
                    saveTasks(tasks)
                }
            }
            "4" -> return
            else -> println("Please choose 1 to 4.")
        }
    }
}
⚠ No in-browser playground here
Kotlin compiles to real JVM bytecode, not something a browser can run directly — running it live would need either a server-side compiler or a third-party embed, the same kind of external dependency this site avoids relying on for a core teaching example. Copy the code below and run it with a real kotlinc on your own machine instead; the “Run It” section explains exactly how.
What each part does — in plain words
data class Task(val done: Boolean, val text: String) — a data class gets a correct equals, hashCode, toString, and copy() for free, from a one-line declaration; the tests below rely on that free equals to compare tasks directly.

fun toLine() / fun fromLine(line: String) — a member function and a companion object factory function that convert a Task to and from the plain-text "0|text"/"1|text" format saved on disk, matching the Java sibling's own persistence format exactly.

File("tasks.txt").readLines() — Kotlin adds this convenience directly onto the JDK's own java.io.File, so reading every line of a file is one call, no explicit stream or scanner required.

fun addTask(tasks: MutableList<Task>, text: String): MutableList<Task> — mutates and returns the same list (mirroring the Python/Go versions' shape) so the tests can check the result without any file I/O at all.
Common mistakes — and how to avoid them
✗ Splitting a saved line on "|" with no limit, so a task whose own text contains a | character breaks the format.
✓ line.split("|", limit = 2) only splits once, no matter how many | characters appear afterward, keeping the rest of the line intact as the task text.
✗ Writing a plain class instead of a data class for Task, then being confused when assertEquals fails for two tasks that look identical.
✓ A plain class compares by reference identity by default; only a data class compares by its actual field values, which is what every test here expects.
✗ Forgetting to re-save the file after every change.
✓ Each branch below that changes the list (add, remove) calls saveTasks immediately afterward, so a crash between operations never silently loses work beyond the last save.

4 Test & Prove Each Part

How do we know this works? We pull the real logic into small, plain functions and check each one against cases we already know the answer to.

Adding a task puts it in the list
Removing the first item leaves the rest untouched
Removing a number that does not exist leaves the list unchanged
Kotlintodo_test.kt
import kotlin.test.Test
import kotlin.test.assertEquals

class TodoTest {
    @Test
    fun addingPutsItInTheList() {
        val tasks = addTask(mutableListOf(), "Buy milk")
        assertEquals(listOf(Task(false, "Buy milk")), tasks)
    }

    @Test
    fun removingTakesTheFirstItemOut() {
        val tasks = mutableListOf(Task(false, "a"), Task(false, "b"))
        assertEquals(listOf(Task(false, "b")), removeTask(tasks, 1))
    }

    @Test
    fun removingAMissingNumberLeavesTheListUnchanged() {
        val tasks = mutableListOf(Task(false, "a"))
        assertEquals(listOf(Task(false, "a")), removeTask(tasks, 9))
    }
}

Compile with kotlinc todo.kt todo_test.kt -include-runtime -d todo.jar and run with JUnit's own runner. addTask/removeTask operate on a plain MutableList<Task>, so the tests never touch tasks.txt at all.

5 The Interface

INPUTmenu choice1-4, then task text or number
What it expects
Choose: 1
New task: Walk the dog
OUTPUTtask listnumbered, saved to tasks.json
What it returns
1. Walk the dog
2. Buy milk

6 Run It & Automate It

Save the code as todo.kt and compile it with kotlinc todo.kt -include-runtime -d todo.jar. It keeps a menu running until you choose Quit.

Run it locally
kotlinc todo.kt -include-runtime -d todo.jar && java -jar todo.jar
Add a task, view the list, remove one by number, then quit — the list is saved to tasks.txt between runs.

A CI tool like Jenkins compiles and tests automatically whenever the code changes — every line below has a plain explanation.

What you should see when it works
Terminala real run
1. Add  2. View  3. Remove  4. Quit
Choose: 1
New task: Buy milk

1. Add  2. View  3. Remove  4. Quit
Choose: 2
1. [ ] Buy milk

1. Add  2. View  3. Remove  4. Quit
Choose: 4
If it breaks — how to fix it
🚨 My tasks are gone the next time I run the program
Check that the program has write access to its own folder, and that you are running it from the same folder each time — tasks.txt is saved relative to the current working directory, not next to the .jar file.
🚨 Removing number 2 removed the wrong task
The numbers shown by “View” are 1-based (matching what a person would naturally type), but the underlying list is 0-based; removeTask already accounts for this internally — just make sure you are reading the current list's numbering, not a stale one from before an earlier add or remove.
GroovyJenkinsfile
// Jenkinsfile &mdash; compiles and tests automatically every time the code changes.
pipeline {
    agent any                                  // run on any available machine

    stages {
        stage('Get the code') {
            steps { checkout scm }             // download the latest code
        }
        stage('Set up Kotlin') {
            steps {
                sh 'kotlinc -version'                             // confirm the compiler is installed
            }
        }
        stage('Compile and test') {
            steps {
                sh 'kotlinc todo.kt todo_test.kt -include-runtime -d build.jar'  // one real JVM jar, no build tool required
                sh 'java -cp build.jar:kotlin-test-junit.jar:junit.jar org.junit.runner.JUnitCore TodoTest'
            }
        }
    }

    post {
        success { echo 'All tests passed.' }
        failure { echo 'A test failed &mdash; look above.' }
    }
}
🎯 Try this next — make it yours

You have a working to-do list. Extend it:

  1. Add a “mark done” option. Toggle a task's done flag instead of only adding/removing. (Teaches: data class's copy() for an immutable update.)
  2. Sort done tasks to the bottom. Change the order View prints them in. (Teaches: sortedBy.)
  3. Validate the remove number. Print a clearer message for an out-of-range number instead of silently doing nothing. (Teaches: returning a result the caller can report on.)
  4. Switch to JSON. Compare this plain-text format against a structured one. (Teaches: trade-offs between a hand-rolled format and a library.)
What you learned
You learned Kotlin’s data class for a value with free, correct equality; a companion object as the idiomatic home for a factory function; and reading/writing plain text files through the JDK's File with Kotlin's own convenience extensions. Related reference: Data Classes, Collections in Kotlin.