๐ŸŽ“ A1Learner
๐Ÿ”
0 of 37 lessons completed

Kotlin Library Project

The library project is one program that manages a small book catalog. It models the books, searches them, groups them, and prints a report.

The catalog data

Four books make up the whole catalog. Every later stage reads this same list shape.

data class Book(val title: String, val author: String, val genre: String, val available: Boolean)

fun main() {
    val books = listOf(
        Book("Dune", "Frank Herbert", "Sci-Fi", true),
        Book("Emma", "Jane Austen", "Classic", true),
        Book("The Hobbit", "J.R.R. Tolkien", "Fantasy", false),
        Book("Neuromancer", "William Gibson", "Sci-Fi", false)
    )
    println(books.size)
    println(books[0].title)
    println(books[2].available)
}

A sealed result for lookups

A lookup ends in exactly one of the two sealed cases. A when over the result covers both and needs no else branch.

data class Book(val title: String, val author: String, val genre: String, val available: Boolean)

sealed class Lookup {
    data class Found(val book: Book) : Lookup()
    data class Missing(val title: String) : Lookup()
}

fun findBook(books: List<Book>, title: String): Lookup {
    val found = books.find { it.title == title }
    return if (found != null) Lookup.Found(found) else Lookup.Missing(title)
}

fun main() {
    val books = listOf(
        Book("Dune", "Frank Herbert", "Sci-Fi", true),
        Book("Emma", "Jane Austen", "Classic", true),
        Book("The Hobbit", "J.R.R. Tolkien", "Fantasy", false),
        Book("Neuromancer", "William Gibson", "Sci-Fi", false)
    )
    val hit = findBook(books, "Emma")
    when (hit) {
        is Lookup.Found -> println("Found: ${hit.book.title}")
        is Lookup.Missing -> println("Missing: ${hit.title}")
    }
    val miss = findBook(books, "Hamlet")
    when (miss) {
        is Lookup.Found -> println("Found: ${miss.book.title}")
        is Lookup.Missing -> println("Missing: ${miss.title}")
    }
}

A generic search function

The search function takes a list of any type plus a lambda that picks its text. It serves books by title, books by author, and plain strings without change.

data class Book(val title: String, val author: String, val genre: String, val available: Boolean)

fun <T> search(items: List<T>, text: String, pick: (T) -> String): List<T> {
    return items.filter { pick(it).contains(text, true) }
}

fun main() {
    val books = listOf(
        Book("Dune", "Frank Herbert", "Sci-Fi", true),
        Book("Emma", "Jane Austen", "Classic", true),
        Book("The Hobbit", "J.R.R. Tolkien", "Fantasy", false),
        Book("Neuromancer", "William Gibson", "Sci-Fi", false)
    )
    println(search(books, "dune") { it.title }.map { it.title })
    println(search(books, "austen") { it.author }.map { it.title })
    println(search(listOf("red", "green", "blue"), "re") { it })
}

Grouping, sorting, and filtering

The call groupBy() sorts the books into a map keyed by genre. Filtering keeps the available books, and sortedBy() orders all of them by title.

data class Book(val title: String, val author: String, val genre: String, val available: Boolean)

fun main() {
    val books = listOf(
        Book("Dune", "Frank Herbert", "Sci-Fi", true),
        Book("Emma", "Jane Austen", "Classic", true),
        Book("The Hobbit", "J.R.R. Tolkien", "Fantasy", false),
        Book("Neuromancer", "William Gibson", "Sci-Fi", false)
    )
    val byGenre = books.groupBy { it.genre }
    println(byGenre.keys)
    println(byGenre["Sci-Fi"]?.map { it.title })
    println(books.filter { it.available }.map { it.title })
    println(books.sortedBy { it.title }.map { it.title })
}

Catching a bad parse

A shelf code arrives as text and must become a number. The runCatching call turns the bad text into a failure value that the program can inspect.

fun main() {
    val good = runCatching { "1965".toInt() }
    println(good.getOrNull())
    println(good.isSuccess)
    val bad = runCatching { "unknown".toInt() }
    println(bad.getOrNull())
    println(bad.isFailure)
    println(bad.getOrElse { -1 })
}

Example

data class Book(val title: String, val author: String, val genre: String, val available: Boolean)

sealed class Lookup {
    data class Found(val book: Book) : Lookup()
    data class Missing(val title: String) : Lookup()
}

fun findBook(books: List<Book>, title: String): Lookup {
    val found = books.find { it.title == title }
    return if (found != null) Lookup.Found(found) else Lookup.Missing(title)
}

fun <T> search(items: List<T>, text: String, pick: (T) -> String): List<T> {
    return items.filter { pick(it).contains(text, true) }
}

fun main() {
    val books = listOf(
        Book("Dune", "Frank Herbert", "Sci-Fi", true),
        Book("Emma", "Jane Austen", "Classic", true),
        Book("The Hobbit", "J.R.R. Tolkien", "Fantasy", false),
        Book("Neuromancer", "William Gibson", "Sci-Fi", false)
    )
    val report = buildString {
        appendLine("Library report")
        for (book in books.sortedBy { it.title }) {
            val (title, author, genre) = book
            append(title)
            append(" by ")
            append(author)
            append(" (")
            append(genre)
            appendLine(")")
        }
        append("Available: ")
        append(books.count { it.available })
    }
    println(report)
    println(search(books, "austen") { it.author }.map { it.title })
    val result = findBook(books, "Dune")
    when (result) {
        is Lookup.Found -> result.book.let { println("Found: ${it.title} by ${it.author}") }
        is Lookup.Missing -> println("Missing: ${result.title}")
    }
    val year = runCatching { "1965".toInt() }.getOrElse { -1 }
    println("Parsed year: $year")
    val badYear = runCatching { "unknown".toInt() }.getOrElse { -1 }
    println("Bad year: $badYear")
}

Key points

  • A data class models each book with a title, an author, a genre, and an availability flag.
  • A sealed class models the two lookup results, found and missing, so a when over them needs no other branch.
  • A generic search function works on a list of any type, as long as a lambda picks the text to search in.
  • Grouping, sorting, and filtering shape the catalog before anything is printed.
  • The runCatching call guards a parse, so bad text becomes a fallback value instead of a crash.
  • The buildString call assembles the whole report, and destructuring names the book parts while entries print.

In this example the full library program runs end to end. The report is built with buildString over the title-sorted catalog, destructuring names each book part as it prints, the generic search finds a book by author, a sealed lookup reports a found book through a let block, and runCatching parses one good year and falls back on one bad year.

Note

All catalog data is fixed inside the program, so every run prints the same report.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Kotlin Playground

Exercise

The catalog groups its books with groupBy { it.genre }. What does the result hold?