Menu
Coddy logo textTech

Singleton Pattern

Part of the Object Oriented Programming section of Coddy's Kotlin journey. Lesson 42 of 57.

A design pattern is a proven solution to a design problem that comes up again and again. The singleton pattern makes sure a class has exactly one instance that the whole program can reach. In Kotlin an object declaration is a singleton: it is created once, on first use, and safely even when several threads start at the same time:

object Settings {
    var theme = "light"
    var fontSize = 14
}

fun openEditor() = "editor in ${Settings.theme} theme"

Inside main:

Settings.theme = "dark"
println(openEditor())
println(Settings.fontSize)

Output:

editor in dark theme
14

The singleton holds state that different parts of the program share. Keep that state private and change it only through methods, so every change goes through the same rules:

object Metrics {
    private val hits = mutableMapOf<String, Int>()

    fun hit(page: String) { hits[page] = (hits[page] ?: 0) + 1 }
    fun report() = hits.entries.joinToString { "${it.key}=${it.value}" }
}

fun home() = Metrics.hit("home")
fun about() = Metrics.hit("about")

Inside main:

home(); about(); home()
println(Metrics.report())

Output:

home=2, about=1

When the single instance needs setup data, a class with a private constructor can create it in its companion object on the first request and return the same instance afterwards:

class Database private constructor(val url: String) {
    companion object {
        private var instance: Database? = null

        fun connect(url: String): Database =
            instance ?: Database(url).also { instance = it }
    }
}

Inside main:

val a = Database.connect("db://main")
val b = Database.connect("db://other")
println(a === b)
println(b.url)

Output:

true
db://main

Use singletons sparingly. A singleton is global state: any code can change it, which hides who depends on it, and tests cannot start from a fresh copy. They fit what is truly one per program, such as the configuration or a logger. When only a part of the program needs an object, pass it in as a parameter, typed as an interface, so a test can pass a fake instead:

interface Clock { fun now(): Long }
object SystemClock : Clock { override fun now() = System.currentTimeMillis() }

class Session(private val clock: Clock) {      // the dependency is visible
    val started = clock.now()
}
val session = Session(SystemClock)             // a test passes a fake Clock
challenge icon

Challenge

Easy

Complete the singleton Logger. It has a changeable minLevel (debug, info, warn or error, in that order). log(level, message) stores [WARN] disk low when the level is at least minLevel and otherwise counts the message in dropped, which outside code can read but not change. entries is a read-only list of the stored lines, and count(level) returns how many stored lines have that level.

The helper functions in Functions.kt are supplied: they reach the same Logger from another file. The supplied code reads the minimum level and then lines level,message, and prints every entry and errors: 1, dropped: 2.

Your code goes in Logger.kt and Functions.kt. Main.kt holds the supplied input/output code and cannot be edited.

Try it yourself

fun main() {
    // Supplied input/output code: keep it as it is
    val input = generateSequence(::readLine).toList()
    Logger.minLevel = input[0]
    for (line in input.drop(1)) {
        val (level, message) = line.split(",", limit = 2)
        when (level) {
            "debug" -> debug(message)
            "info" -> info(message)
            "warn" -> warn(message)
            else -> error(message)
        }
    }
    for (e in Logger.entries) println(e)
    println("errors: ${Logger.count("error")}, dropped: ${Logger.dropped}")
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Kotlin playground