object / Singletons
Kotlin’s object keyword has three uses: singleton object declaration, anonymous object expression, and companion objects (Java static equivalent). All three avoid boilerplate while staying type-safe.
Singleton, anonymous, companion, expression
EXAMPLE
// 1) Object declaration — singleton
object Logger {
private val logs = mutableListOf<String>()
fun info(msg: String) {
val entry = "[INFO] \${System.currentTimeMillis()}: \$msg"
logs.add(entry)
println(entry)
}
fun warn(msg: String) {
val entry = "[WARN] \${System.currentTimeMillis()}: \$msg"
logs.add(entry)
println(entry)
}
fun all(): List<String> = logs.toList()
}
// Use:
Logger.info("Server started")
Logger.warn("Disk full")
Logger.all().forEach(::println)
// Thread-safe by default (Kotlin lazy initialization for singletons)
// 2) Companion object — static-like members on a class
class User private constructor(val id: Long, val name: String) {
companion object {
private const val MAX_NAME_LENGTH = 100
fun create(name: String): User {
require(name.length <= MAX_NAME_LENGTH) { "name too long" }
return User(id = generateId(), name = name)
}
fun fromMap(m: Map<String, Any>): User {
return User(
id = m["id"] as Long,
name = m["name"] as String,
)
}
private fun generateId(): Long = System.nanoTime()
}
}
// Use:
val u1 = User.create("Ada")
val u2 = User.fromMap(mapOf("id" to 1L, "name" to "Bo"))
// 3) Named companion object
class Server {
companion object Factory {
fun default() = Server()
fun forPort(port: Int) = Server().apply { /* ... */ }
}
}
Server.default()
Server.Factory.forPort(8080) // explicit (rare)
// 4) Anonymous object expression — one-off interface implementations
interface ClickHandler {
fun onClick(view: View)
fun onLongClick(view: View): Boolean
}
val handler = object : ClickHandler {
override fun onClick(view: View) { println("clicked") }
override fun onLongClick(view: View): Boolean {
println("long clicked")
return true
}
}
// Or extending a class + interface
val listener = object : MouseAdapter(), ClickHandler {
override fun mouseClicked(e: MouseEvent) { /* ... */ }
override fun onClick(view: View) { /* ... */ }
override fun onLongClick(view: View) = false
}
// 5) Anonymous with captured state
fun makeCounter(): () -> Int {
val obj = object {
var count = 0
}
return { obj.count++; obj.count }
}
val next = makeCounter()
println(next()) // 1
println(next()) // 2
// 6) Object literal with custom equality
val point = object {
val x = 10
val y = 20
}
// Note: object literal type can only be used inside the same function
// (not exposed publicly)
// 7) Constants — companion object with const
class Config {
companion object {
const val DEFAULT_TIMEOUT = 30_000 // compile-time constant
const val MAX_RETRIES = 3
const val API_VERSION = "v1"
val cachedDate = Date() // runtime-initialised
}
}
println(Config.DEFAULT_TIMEOUT)
// 8) Implementing interfaces in companion object
class Currency private constructor(val code: String) {
companion object : Comparator<Currency> {
override fun compare(a: Currency, b: Currency) = a.code.compareTo(b.code)
}
}
// Use the companion as a Comparator directly
val sorted = currencies.sortedWith(Currency)
// 9) Factory pattern via companion
sealed class Shape {
companion object {
fun circle(radius: Double): Shape = Circle(radius)
fun square(side: Double): Shape = Rectangle(side, side)
fun rectangle(w: Double, h: Double) = Rectangle(w, h)
}
}
data class Circle(val radius: Double) : Shape()
data class Rectangle(val w: Double, val h: Double) : Shape()
// Hide the constructors, expose factories
val s = Shape.circle(5.0)
// 10) Object as a state machine
sealed class State {
object Idle : State()
object Running : State()
object Done : State()
data class Failed(val error: String) : State()
}
fun handle(state: State) = when (state) {
State.Idle -> "waiting"
State.Running -> "working"
State.Done -> "finished"
is State.Failed -> "failed: \${state.error}"
}
// Object instances Idle, Running, Done are singletons — only one each.
// Failed is a data class with state.
// 11) Object expression in Android — RecyclerView adapter
class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = object : ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
) {}
// ...
}
// 12) Test doubles — anonymous object as a stub
interface UserRepository { fun findById(id: Long): User? }
fun test() {
val mockRepo = object : UserRepository {
override fun findById(id: Long) = User(id, "Test")
}
val service = UserService(mockRepo)
assert(service.getName(1) == "Test")
}
// 13) Object expression vs lambda
// Lambda: single-method interface (functional interface)
fun setListener(onClick: () -> Unit) { /* ... */ }
setListener { println("clicked") }
// Object expression: multiple methods or extending a class
fun setHandler(handler: ClickHandler) { /* ... */ }
setHandler(object : ClickHandler {
override fun onClick(v: View) { /* ... */ }
override fun onLongClick(v: View) = false
})
// 14) Companion object extensions (extend a class through its companion)
fun User.Companion.fromJson(json: String): User {
/* parse JSON */
return User.create(/* ... */)
}
User.fromJson("{...}") // looks like a static method
// 15) Java interop
// To call Logger.info("") from Java:
// Logger.INSTANCE.info("hello")
// Or annotate with @@JvmStatic to make it a real Java static:
object Logger2 {
@@JvmStatic
fun info(msg: String) = println("[INFO] \$msg")
}
// Java: Logger2.info("hello")
class Server {
companion object {
@@JvmStatic
fun default() = Server()
}
}
// Java: Server.default()
// 16) Sealed + object — exhaustive matching
sealed class Result<out T>
object Loading : Result<Nothing>()
data class Success<T>(val value: T) : Result<T>()
data class Failure(val error: String) : Result<Nothing>()
fun describe(r: Result<String>) = when (r) {
Loading -> "loading"
is Success -> "value: \${r.value}"
is Failure -> "failed: \${r.error}"
}
// Loading is a singleton — there's only ONE Loading instance.
// 17) Common patterns
// a) Configuration singleton
object AppConfig {
var apiUrl: String = "https://api.example.com"
var timeout: Int = 30_000
fun load(env: Map<String, String>) {
apiUrl = env["API_URL"] ?: apiUrl
timeout = env["TIMEOUT"]?.toIntOrNull() ?: timeout
}
}
// b) DI registry (manual)
object ServiceLocator {
val httpClient: HttpClient by lazy { HttpClient() }
val userRepo: UserRepository by lazy { UserRepositoryImpl(httpClient) }
val authService: AuthService by lazy { AuthService(userRepo) }
}
// c) Constants namespace
object Constants {
const val MAX_USERS = 1000
val SUPPORTED_LOCALES = listOf("en", "fr", "es")
}
// 18) Common bugs
// ❌ Calling an object's method 10000 times in a loop with global state — race conditions
// ❌ Using object for stateful per-user data (singleton = shared across users)
// ❌ Forgetting @@JvmStatic when Java interop matters
// ❌ Storing growing data in companion object (memory leak — singletons live forever)
// ❌ Confusing 'object expression' (anonymous, returns instance) with 'object declaration' (named singleton)
// 19) Best practices
// ✅ Use object for stateless utilities + singletons
// ✅ Use companion object for factory methods + constants
// ✅ Use @@JvmStatic when Java callers need real static methods
// ✅ Use sealed class + object for state machines with constant variants
// ✅ Use object expression for one-off interface implementations
// ✅ For DI, prefer Koin / Dagger / Hilt over ServiceLocator pattern
// ✅ Don't store mutable state in object singletons unless threading is handled
Why it matters
object covers three needs: object Logger for singletons, companion object for factory methods + constants, anonymous object : Interface { } for one-off implementations. Pair with @JvmStatic when Java callers expect real static methods.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…